Currently working on a project where I need to dynamically load other components in the main application system, I'm facing an issue with some error that throws a Jest Invariant constraint violation when trying to import dynamically a module.
An example is accessible at https://gitlab.com/matthieu88160/stryker-issue
In this project, I have a class used to load a component and another that will use the ComponentLoader and add some check and post-processing (this part of the code is not present in the accessible project). In the GitLab job, two test suites are executed and one fails. My problem is that the two test suites are exactly the same, obviously, I copy-pasted the first one to create. The second one and by the way demonstrate the issue.
I already tried some workaround found on the web without any success.
Here is the code of the component I try to test :
import {existsSync} from 'fs';
import {format} from 'util';
export default class ComponentLoader
{
static load(file) {
if(existsSync(file)) {
return import(file);
}
throw new Error(
format('Component file "%s" does not exist. Cannot load module', file)
);
}
}
And the test itself:
import {describe, expect, test} from '@jest/globals';
import {format} from 'util';
import {fileURLToPath} from 'url';
import {dirname} from 'path';
import mock from 'jest-mock';
describe(
'ComponentLoader',
() => {
describe('load', () => {
test('load method is able to load a component from a file', () => new Promise(
resolve => {
Promise.all(
[import('../src/ComponentLoader.mjs'), import('./fixture/component/A1/component.fixture.mjs')]
).then(modules => {
const file = format(
'%s/./fixture/component/A1/component.fixture.mjs',
dirname(fileURLToPath(import.meta.url))
);
modules[0].default.load(file).then(obj => {
expect(obj).toBe(modules[1]);
resolve();
});
});
})
);
});
}
);
And here the error report:
PASS test/ComponentLoaderA.test.mjs
FAIL test/ComponentLoaderB.test.mjs
● ComponentLoader › load › load method is able to load a component from a file
15 | static load(file) {
16 | if(existsSync(file)) {
> 17 | return import(file);
| ^
18 | }
19 |
20 | throw new Error(
at invariant (node_modules/jest-runtime/build/index.js:2004:11)
at Function.load (src/ComponentLoader.mjs:17:13)
at test/ComponentLoaderB.test.mjs:21:44
The interesting element from my point of view is the fact the ComponentLoaderA.test.mjs
and the ComponentLoaderB.test.mjs
are exactly the same.
The full error trace I found is:
CComponentLoader load load method is able to load a component from a file
Error:
at invariant (importTest/.stryker-tmp/sandbox7042873/node_modules/jest-runtime/build/index.js:2004:11)
at Runtime.loadEsmModule (importTest/.stryker-tmp/sandbox7042873/node_modules/jest-runtime/build/index.js:534:7)
at Runtime.linkModules (importTest/.stryker-tmp/sandbox7042873/node_modules/jest-runtime/build/index.js:616:19)
at importModuleDynamically (importTest/.stryker-tmp/sandbox7042873/node_modules/jest-runtime/build/index.js:555:16)
at importModuleDynamicallyWrapper (internal/vm/module.js:443:21)
at exports.importModuleDynamicallyCallback (internal/process/esm_loader.js:30:14)
at Function.load (importTest/.stryker-tmp/sandbox7042873/src/ComponentLoader.mjs:89:11)
at importTest/.stryker-tmp/sandbox7042873/test/ComponentLoaderB.test.mjs:22:37
at new Promise (<anonymous>)
at Object.<anonymous> (importTest/.stryker-tmp/sandbox7042873/test/ComponentLoaderB.test.mjs:14:79)
It seems the error does not have any message.
Further information from the jest-runtime investigation :
It seems that between the tests, the sandbox context is lost for a reason I cannot be able to manage to find at the moment.
In node_modules/jest-runtime/build/index.js:2004:11 : The condition is NULL, the error is then thrown without a message.
function invariant(condition, message) {
if (!condition) {
throw new Error(message);
}
}
In node_modules/jest-runtime/build/index.js:534:7 : The context is NULL, and no message given, creating my empty error message.
const context = this._environment.getVmContext();
invariant(context);
The toString() of this._environment.getVmContext method as follow:
getVmContext() {
return this.context;
}
The current _environment presents a null context :
NodeEnvironment {
context: null,
fakeTimers: null,
[...]
}
The deeper point I can reach is this code where the context appear to be null :
const module = new (_vm().SourceTextModule)(transformedCode, {
context,
identifier: modulePath,
importModuleDynamically: (specifier, referencingModule) => {
return this.linkModules(
specifier,
referencingModule.identifier,
referencingModule.context
)},
initializeImportMeta(meta) {
meta.url = (0, _url().pathToFileURL)(modulePath).href;
}
});
The context variable is not empty and _vm().SourceTextModule
is a class extending Module.
I can notice in the importModuleDynamically
execution using console.log(this._environment)
that the context is currently null.