Create React App - Unable to set window object before test runs - javascript

Trying to set up some tests for a component in a CRA-generated application.
Having an issue where the test is failing to run due to an imported file relying on a window object which isn't initialised.
Quick overview:
In public/index.html there is an object defined on the window called window.config.
<script src="%PUBLIC_URL%/config.js"></script>
<script>
// THESE PROPERTIES ARE DEFINED IN config.js
window.config = {
ENV,
IDENTITY_URL,
OIDC_CLIENT_ID,
};
</script>
There is a file called identity.constants.js which exports an object that uses these window variables.
export const IdentityConfig = {
authority: window.config.IDENTITY_URL,
client_id: window.config.OIDC_CLIENT_ID,
... etc
};
The component I am trying to test imports another component that relies on identity.constants.js shown above.
The problem is that the test fails to start and throws an error stating window.config is undefined from the identity.constants.js file.
After reading the CRA docs, I have tried adding the following code into src/setupTests.js to set the config object before the test starts but it seems to fail before this code runs.
global.config = {
ENV: "development",
IDENTITY_URL: "identityurl.com",
OIDC_CLIENT_ID: "i-am-a-clientId",
... etc
};
I'm looking for a way to set this window variable before the tests run (or a better way to structure the way I'm using window variables) so I can successfully run my tests.

globa.config defines a global variable named config, you'll need to define global.window firstly, then add config as a property of global.window.
It seems your test environment is node, if you have jsdom as the devDependencies of your project, try to add below piece of code to your setupTests.js:
import { JSDOM } from 'jsdom';
const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
global.window = jsdom.window;
global.document = jsdom.window.document;
global.window.config = {
ENV: "development",
IDENTITY_URL: "identityurl.com",
OIDC_CLIENT_ID: "i-am-a-clientId",
... etc
};
Without jsdom, a simple fix is like below:
global.window = {
config: {
ENV: "development",
IDENTITY_URL: "identityurl.com",
OIDC_CLIENT_ID: "i-am-a-clientId",
... etc
}
}

Related

Can't mock default export in tested file

I'm working on a Typescript project and I'm trying to add tests for functions that are sorting through MongoDB responses to get meaningful results. I've set up an in-memory mock of the database, now I just need to somehow get the test to use this mock database when running tests on the functions in my file.
I have a file called mongoClient.ts that is responsible for this MongoClient object that I need to mock:
import { MongoClient } from 'mongodb';
const client = new MongoClient(`${process.env.MONGO_CONNECT}`);
export default client;
The file that I need to test, called models.ts, imports mongoClient with an ES6 import statement:
import client from '../mongoClient';
In my test I have created a MongoMemoryServer (mongodb-memory-server), and I have made a class to handle the connection to this MongoMemoryServer:
class dbConnection {
server;
connection;
uri;
async init() {
this.server = await MongoMemoryServer.create();
this.uri = await this.server.getUri();
this.connection = await new MongoClient(this.uri);
return this.connection;
}
connect() {
this.connection.connect();
}
getClient() {
return this.connection;
}
getUri() {
return this.uri;
}
async stopIt() {
await this.server.stop();
}
}
I cannot seem to get the models object that I create in the test (with const models = require('../src/models');) to mock its import of 'mongoClient'.
I've tried the following:
let mock_db = new dbConnection();
jest.doMock('../src/mongoClient', () => ({
__es6Module: true,
default: mock_db.getClient()
}));
I tried to use mock(...) earlier; that throws this error when I run the test:
The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables.
In a beforeAll(...) I am able to fill this DB with dummy information so my functions in models will run correctly, but these functions can't see the mock in-memory database because I can't properly mock the mongoClient file that gets imported at the top of my models.ts. I don't want to have to change any code that isn't in a .test file to accomplish this, but I can't get this test environment to point at my mock in-memory database while running these tests. I must be missing something about the implementation of doMock, and I need some help to fix it.
- -EDIT_1- - I think this might have something to do with the order in which I'm declaring things. The class dbConnection is declared directly under my imports at the top of the test file. Then under that I instantiate the dbConnection class as mockDb and jest.doMock(... like in the example shown above. My tested function seems to be trying to use a mocked import that only contains the following:
{"__es6Module":true}
The error message I'm getting at this point is ERROR --- _get__(...).db is not a function.
The problem with this could be that it's mocking the imported module correctly, but the default export used to mock (mockDb.getClient()) is undefined when it's declared in that doMock statement near the top of the file. I don't know where else to move that doMock, and it breaks if I move it into the top-level beforeAll or into the describe section for these tests or into the test itself.
- -EDIT_2- - Changing __es6Module to __esModule (because the compiled js file looks for __esModule) makes it so that undefined is the contents of the imported class in my code.
The compiled js code has the following at the top:
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const mongoClient_1 = __importDefault(require("../util/mongoClient"));
- -FINAL_EDIT- - I've figured this out, it was an error on my part in how the order of the Jest tests were being executed. The default export client that I was mocking had not been initialized before the tests were being run. This required that I mock the default import in a beforeAll(...) statement inside of each 'describe' block of tests, and in each test I have to instantiate a new models at the start of the test with const models = require(...);. I have working tests now, and everything is good in life ;-)
I've figured this out, it was an error on my part in how the order of the Jest tests were being executed. The default export client that I was mocking had not been initialized before the tests were being run. This required that I mock the default import in a beforeAll(...) statement inside of each 'describe' block of tests, and in each test I have to instantiate a new models at the start of the test with const models = require(...);. I have working tests now

Vue.js 2 + Typescript - global variables becomes undefined after build

I have a Vue.js 2 project with Typescript. In the main.ts file, I've declared 2 variables, that I've wanted to access globally in my project:
// ...
Vue.prototype.$http = http; // this is the library imported from another file, contains various methods such as `get`, `post` etc.
Vue.prototype.$urls = urls; // this is JSON object, also imported from another file
new Vue({
store,
render: (h) => h(App),
}).$mount('#app');
In one of my components, let's call it User I have following mounted code block:
mounted(): void {
this.$http.get(`${this.$urls.getUser}/${this.userId}`);
}
Everything works fine when I'm running a local server (via npm run serve command), but when I create an app build (via npm run build command) and enter the app on the server (or the index.html file on my hdd) I receive following error:
TypeError: Cannot read property 'get' of undefined
at VueComponent.value (user.ts:62) // <-- this line is the one with $http.get from `mounted` hook
I'm not sure how to proceed with this, I've blindly tried to add those global values to various places e.g. in http.d.ts file I have the following:
import { KeyableInterface } from '#/interfaces/HelperInterfaces';
import Vue from 'vue';
declare module 'vue/types/vue' {
interface VueConstructor {
$http: KeyableInterface;
}
}
declare module 'vue/types/vue' {
interface Vue {
$http: KeyableInterface;
}
}
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
http?: KeyableInterface
}
}
(I've also created urls.d.ts with similar code)
UPDATE #1:
I've tried also following approach - in my main.ts file:
const helperModules = {
/* eslint-disable-next-line #typescript-eslint/no-explicit-any */
install: (vueInstance: any) => {
vueInstance.prototype.$http = http;
vueInstance.prototype.$urls = urls;
},
};
Vue.use(helperModules);
But it still doesn't work (same error).
UPDATE #2:
I've also imported http utility into my user component, and added following console.log to existing mounted callback:
console.log(http, this.$http)
And while working on my localhost, it returns me twice the same value, but when I create a build it returns me:
Module {__esModule: true, Symbol(Symbol.toStringTag): "Module"}, undefined
Similar thing happens, when I add console.log(urls, this.$urls) - imported module is being logged, while prototyped one returns undefined.
Any thoughts? Will appreciate any help.
Finally I've overcome the problem by moving the prototyping parts from main.ts to App.ts file.
I'm not 100% sure if it's a valid "the Vue.js way" of solving this, as I've always declared that in main.js file - but I was using then JavaScript & it was "just working" as expected.

How to use an external non vue script in vue

I try to use an external script (https://libs.crefopay.de/3.0/secure-fields.js) which is not vue based
I added the script via -tags into index.html
But when I try to intsanciate an object, like in the excample of the script publisher.
let secureFieldsClientInstance =
new SecureFieldsClient('xxxxx',
this.custNo,
this.paymentRegisteredCallback,
this.initializationCompleteCallback,
configuration)
Vue says "'SecureFieldsClient' is not defined"
If I use this.
let secureFieldsClientInstance =
new this.SecureFieldsClient('xxxxx',
this.custNo,
this.paymentRegisteredCallback,
this.initializationCompleteCallback,
configuration)
secureFieldsClientInstance.registerPayment()
Vue says: Error in v-on handler: "TypeError: this.SecureFieldsClient is not a constructor"
My Code:
methods: {
startPayment () {
this.state = null
if (!this.selected) {
this.state = false
this.msg = 'Bitte Zahlungsweise auswählen.'
} else {
localStorage.payment = this.selected
let configuration = {
url: 'https://sandbox.crefopay.de/secureFields/',
placeholders: {
}
}
let secureFieldsClientInstance =
new SecureFieldsClient('xxxxx',
this.custNo,
this.paymentRegisteredCallback,
this.initializationCompleteCallback,
configuration)
secureFieldsClientInstance.registerPayment()
// this.$router.replace({ name: 'payment' })
}
}
}
Where is my mistake?
EDIT:
Updated the hole question
Here is a minimal Vue app for the context your provided, which works:
https://codepen.io/krukid/pen/voxqPj
Without additional details it's hard to say what your specific problem is, but most probably the library gets loaded after your method executes, so window.SecureFieldsClient is expectedly not yet defined. Or, there is some runtime error that crashes your script and prevents your method from executing. There could be some other more exotic issues, but lacking a broader context I can only speculate.
To ensure your library loads before running any code from it, you should attach an onload listener to your external script:
mounted () {
let crefPayApi = document.createElement('script')
crefPayApi.onload = () => this.startPayment()
crefPayApi.setAttribute('src', 'https://libs.crefopay.de/3.0/secure-fields.js')
document.head.appendChild(crefPayApi)
},
I found the solution.
the import was never the problem.
I had just to ignore VUEs/eslints complaining about the missing "this" via // eslint-disable-next-line and it works.
So external fuctions/opbjects should be called without "this" it seems.
let secureFieldsClientInstance =
new SecureFieldsClient('xxxxx',
this.custNo,
this.paymentRegisteredCallback,
this.initializationCompleteCallback,
configuration)
You could download the script and then use the import directive to load the script via webpack. You probably have something like import Vue from 'vue'; in your project. This just imports vue from your node modules.
It's the exact same thing for other external scripts, just use a relative path. When using Vue-CLI, you can do import i18n from './i18n';, where the src folder would contain a i18n.js
If you really want to use a CDN, you can add it like you normally would and then add it to the externals: https://webpack.js.org/configuration/externals/#externals to make it accessible from within webpack

Webpack how to shim module wrapped in window closure?

I have some 3rd party code that is exported with browser-only intentions. It is wrapped in a self-invoking function that expects window to be passed.
(function (window) { "use strict";
window['module'] = {}
})(window);
Is there a better name to describe this style module?
I would like to use webpack to require or import this code.
currently using webpack#3.5.1 I need this to work in Node.js and ES6 environments.
I was able to achieve this by using the imports-loader and exports-loader
rules: [
{
test: /MyModule\.js/,
use: [
"imports-loader?window=>{}",
"exports-loader?window.MyModule"
]
},
Unless the author updates its code to UMD (or similar) there would be no way for you to require/import it.
I can't think of a way you could actually make it work without the author's modification.
Although, with the help of the window package you could use the following trick :
//in a separate file
const Window = require("window");
const window = new Window();
global.window = window; //Try with and without this
require("my_module");
module.exports = window["module_global_variable_name"];
But that would only work if the author didn't use any other global variables (eg. fetch instead of window.fetch would ruin the trick).
An alternative to trying to require/import from window is to desctructure it after your import declarations. This gives you a importish feel without jumping through hoops.
// Normal Imports
import Foo from "Foo";
// Globals from window
const {
Bar
} = window;

Jest global variables dynamically imported from a JS file (export.module)

I need to import a .js file with config values to be used on my react app:
import config from './config'
These values are already added at webpack configuration:
new webpack.DefinePlugin({...config})
What I need is to import these values into jest.config.js:
globals: {
config: // Here config...
}
"I know that we can add these values manually, but I want to add them from this file to prevent the maintenance of all values instead".
Thanks!
https://jestjs.io/docs/en/configuration#globals-object
Note that, if you specify a global reference value (like an object or array) here, and some code mutates that value in the midst of running a test, that mutation will not be persisted across test runs for other test files. In addition the globals object must be json-serializable, so it can't be used to specify global functions.
here is a guide on adding globals to your jest config with basic usage below
import * as config from "path/to/config";
//...package.json || jest.config
"jest": {
"globals": {
...config
}
}
const config = {
this: 'is now global'
}
console.log({
jest: {
globals: {
...config
}
}
})
You can add it to the globals object and import the config as normal

Categories

Resources