NodeJs import issue with Jest - node.js

Issue on facebook/jest
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.

Related

Mocking of a function within a function not working in jest with jest.spyOn

I'm trying to write a test for a function that downloads an Excel file within my React app.
I understand that I need to mock certain functionality, but it doesn't seem to be working according to everything that I have read online.
A basic mock that works is this:
import FileSaver from 'file-saver'
import { xlsxExport } from './functions'
// other code...
test('saveAs', async () => {
const saveAsSpy = jest.spyOn(FileSaver, 'saveAs')
FileSaver.saveAs('test')
expect(saveAsSpy).toHaveBeenCalledWith('test')
})
The above works: FileSaver.saveAs was successfully mocked. However, I am utilising FileSaver.saveAs within another function that I wish to test and the mocking does not seem to transfer into that. functions.ts and functions.tests.ts below.
functions.ts:
import { Dictionary } from './interfaces'
import * as ExcelJS from 'exceljs'
import FileSaver from 'file-saver'
export function xlsxExport(data: Dictionary<any>[], fileName?: string, tabName?: string) {
const workbook = new ExcelJS.Workbook()
const worksheet = workbook.addWorksheet(tabName || 'export')
// Get columns from first item in data
worksheet.columns = Object.keys(data[0]).map((key: string) => ({ header: key, key: key }))
// Write each item as a row
for (const row of data) {
worksheet.addRow(row)
}
// Download the file
workbook.xlsx.writeBuffer().then(function (buffer) {
const blob = new Blob([buffer], { type: 'applicationi/xlsx' })
FileSaver.saveAs(blob, (fileName || 'excel_export') + '.xlsx')
})
}
functions.tests.ts
import FileSaver from 'file-saver'
import { xlsxExport } from './functions'
// ...other code
test('xlsxExport', async () => {
const saveAsSpy = jest.spyOn(FileSaver, 'saveAs')
xlsxExport(myArrayOfDicts, 'test_download')
expect(saveAsSpy).toHaveBeenCalledWith('something, anything')
})
Error:
TypeError: Cannot read properties of null (reading 'createElement')
at Function.saveAs (C:\dev\pcig-react\node_modules\file-saver\src\FileSaver.js:92:9)
at C:\dev\pcig-react\src\common\functions.ts:221:19
at processTicksAndRejections (node:internal/process/task_queues:95:5)
Node.js v19.3.0
FAIL src/common/functions.test.ts
● Test suite failed to run
Jest worker encountered 4 child process exceptions, exceeding retry limit
at ChildProcessWorker.initialize (node_modules/jest-runner/node_modules/jest-worker/build/workers/ChildProcessWorker.js:185:21)
It is trying to call the non-mocked FileSaver.saveAs (line 221 of my file) within xlsxExport.
How can I get it to call the mocked version?

Firebase Realtime Database not working on NextJS only after build - Maximum call stack size exceeded

I used Firebase Realtime Database with Next.js, for back-end.
And I wrote the below code. But It works only in local, not in production. The reason I comments some lines as follows is that I want to show that the error was not caused by my code. onValue, get, set, update were all the same. The same error was raised. And also I confirmed that effect function in useEffect() runs only once.
/* lib/firebase.ts */
import { initializeApp } from 'firebase/app';
import { getDatabase } from 'firebase/database';
// Initialize Firebase
const firebaseConfig = {
...
};
const app = initializeApp(firebaseConfig);
// Initialize Realtime Database
export const database = getDatabase(app);
import { onValue, ref, update } from 'firebase/database';
import { database } from 'lib/firebase';
...
useEffect(() => {
onValue(
ref(database, `objects/${user.uid}`),
(snapshots) => {
console.log(snapshots);
// const fetchedObjects: Common.Object[] = [];
// snapshots.forEach((snapshot) => {
// fetchedObjects.push({ key: snapshot.key, ...snapshot.val() } as Common.Object);
// });
// setTimeout(() => {
// setObjects(fetchedObjects);
// setIsObjectLoaded(true);
// }, 500);
}
);
}, []);
As a result of console.log(), a snapshot object with no children was returned. But It should be a snapshot object with serveral child nodes according to database state. It is the case in local.
Only in production, the below error was raised. _app-e9aaf40698fc4780.js was bundled by Next.js, so It was so complicated that I couldn't read it. But when I deduce from comments, maybe it can be firebase/database code.
Uncaught RangeError: Maximum call stack size exceeded
at i (_app-e9aaf40698fc4780.js:1486:770)
at i (_app-e9aaf40698fc4780.js:1486:897)
at i (_app-e9aaf40698fc4780.js:1486:906)
at i (_app-e9aaf40698fc4780.js:1486:897)
at i (_app-e9aaf40698fc4780.js:1486:906)
at i (_app-e9aaf40698fc4780.js:1486:897)
at i (_app-e9aaf40698fc4780.js:1486:906)
at i (_app-e9aaf40698fc4780.js:1486:897)
at i (_app-e9aaf40698fc4780.js:1486:906)
at i (_app-e9aaf40698fc4780.js:1486:897)
1486 line's code is here.
*/ let e3,e4,e6=new class extends eW{compare(e,t){let n=e.node.getPriority(),r=t.node.getPriority(),i=n.compareTo(r);return 0===i?q(e.name,t.name):i}isDefinedOn(e){return!e.getPriority().isEmpty()}indexedValueChanged(e,t){return!e.getPriority().equals(t.getPriority())}minPost(){return eq.MIN}maxPost(){return new eq(j,new e2("[PRIORITY-POST]",e4))}makePost(e,t){let n=e3(e);return new eq(t,new e2("[PRIORITY-POST]",n))}toString(){return".priority"}},e5=Math.log(2);class e8{constructor(e){var t;this.count=parseInt(Math.log(e+1)/e5,10),this.current_=this.count-1;let n=parseInt(Array(this.count+1).join("1"),2);this.bits_=e+1&n}nextBitIsOne(){let e=!(this.bits_&1<<this.current_);return this.current_--,e}}let e9=function(e,t,n,r){e.sort(t);let i=function(t,r){let s=r-t,o,a;if(0===s)return null;if(1===s)return o=e[t],a=n?n(o):o,new eG(a,o.node,eG.BLACK,null,null);{let l=parseInt(s/2,10)+t,u=i(t,l),c=i(l+1,r);return o=e[l],a=n?n(o):o,new eG(a,o.node,eG.BLACK,u,c)}},s=new e8(e.length),o=function(t){let r=null,s=null,o=e.length,a=function(t,r){let s=o-t;o-=t;let a=i(s+1,o),u=e[s],c=n?n(u):u;l(new eG(c,u.node,r,null,a))},l=function(e){r?(r.left=e,r=e):(s=e,r=e)};for(let u=0;u<t.count;++u){let c=t.nextBitIsOne(),h=Math.pow(2,t.count-(u+1));c?a(h,eG.BLACK):(a(h,eG.BLACK),a(h,eG.RED))}return s}(s);return new eY(r||t,o)},e7,te={};class tt{constructor(e,t){this.indexes_=e,this.indexSet_=t}static get Default(){return(0,f.hu)(te&&e6,"ChildrenNode.ts has not been loaded"),e7=e7||new tt({".priority":te},{".priority":e6})}get(e){let t=(0,f.DV)(this.indexes_,e);if(!t)throw Error("No index defined for "+e);return t instanceof eY?t:null}hasIndex(e){return(0,f.r3)(this.indexSet_,e.toString())}addIndex(e,t){(0,f.hu)(e!==e$,"KeyIndex always exists and isn't meant to be added to the IndexMap.");let n=[],r=!1,i=t.getIterator(eq.Wrap),s=i.getNext();for(;s;)r=r||e.isDefinedOn(s.node),n.push(s),s=i.getNext();let o;o=r?e9(n,e.getCompare()):te;let a=e.toString(),l=Object.assign({},this.indexSet_);l[a]=e;let u=Object.assign({},this.indexes_);return u[a]=o,new tt(u,l)}addToIndexes(e,t){let n=(0,f.UI)(this.indexes_,(n,r)=>{let i=(0,f.DV)(this.indexSet_,r);if((0,f.hu)(i,"Missing index implementation for "+r),n===te){if(!i.isDefinedOn(e.node))return te;{let s=[],o=t.getIterator(eq.Wrap),a=o.getNext();for(;a;)a.name!==e.name&&s.push(a),a=o.getNext();return s.push(e),e9(s,i.getCompare())}}{let l=t.get(e.name),u=n;return l&&(u=u.remove(new eq(e.name,l))),u.insert(e,e.node)}});return new tt(n,this.indexSet_)}removeFromIndexes(e,t){let n=(0,f.UI)(this.indexes_,n=>{if(n===te)return n;{let r=t.get(e.name);return r?n.remove(new eq(e.name,r)):n}});return new tt(n,this.indexSet_)}}/**
I can't interpret the code, and I can't figure out the cause, so I'm going crazy. Help me.
+) I also tested on the new Next.js project created by create-next-app.
npx create-next-app#latest --typescript
cd {project folder name}
npm install firebase
And the code I wrote is here.
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { useEffect } from "react";
import { initializeApp } from "firebase/app";
import { getDatabase, onValue, ref } from "firebase/database";
function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
const firebaseConfig = {
...
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
// It works only in local
onValue(ref(db, "???"), (snapshots) => console.log(snapshots));
}, []);
return <Component {...pageProps} />;
}
export default MyApp;
As a result, like my project, the same error was raised. But It works well in local.
I'm the author of this post.
I resolved this problem by downgrading Next.js version from 12.3.1 to 12.3.0.

How do I set up dynamic imports correctly (for beyond localhost)?

I followed https://docs.meteor.com/packages/dynamic-import.html to set up dynamic imports, and it works fine on localhost.
For context, I am creating a blog (Meteor/React/Apollo) which renders MDX files, and these files need to be imported, so I have a list of all my posts as such:
import("./imports/posts/61a000d03a1931b8819dc17e.mdx")
import("./imports/posts/619cae2f03f4ff710aa3d980.mdx")
import("./imports/posts/619e002d386ebf2023ea85c3.mdx")
import("./imports/posts/619fff7c5b312d7622acda86.mdx")
I have a Post.jsx component:
import React, { useState, useRef } from "react"
import { useHistory, useParams } from "react-router-dom"
import { useQuery } from "#apollo/client"
import { GET_POST_ID } from "../../api/posts/queries"
const Post = () => {
const Post = useRef()
const history = useHistory()
const { slug } = useParams()
const [loadedPost, setLoaded] = useState(false)
const [viewer, showViewer] = useState(false)
const open = () => showViewer(true)
const { data, loading, error } = useQuery(GET_POST_ID, { variables: { slug }})
if (loading) return null
if (error) {
console.log(error)
return null
}
import(`./posts/${data._id}.mdx`).then(MDX => {
Post.current = MDX.default
setLoaded(true)
}, (err) => {
console.log(err)
})
return loadedPost ? (
<>
<div className="postContent">
<div className="markdownOverride markdown-body">
<Post.current />
</div>
</div>
</>
) : null
}
export default Post
This works well and good on my local network. However, if I attempt to access it from outside my local network, an error is thrown in the console that all the blog modules are not found. The Apollo/GraphQL portion works fine, but the actual module can't be imported.
How do I get this to work outside of localhost?
Thanks.
EDIT: The error messages are, for each post:
Uncaught (in promise) Error: Cannot find module '/imports/posts/61a000d03a1931b8819dc17e.mdx`
And when I load the actual post page:
Uncaught (in promise) TypeError: Failed to fetch
Isn't your error thrown by console.log(err) ?
import(`./posts/${data._id}.mdx`).then(MDX => {
Post.current = MDX.default
setLoaded(true)
}, (err) => {
console.log(err) // <---- here
})
This means your path isn't right for /imports/posts/61a000d03a1931b8819dc17e.mdx.
To me you can't use changing parameters when doing dynamic imports.
./posts/${data._id}.mdx, because your meteor or webpack compilation needs to treat all the data._ids avalaible in your database in order to compile and prepare the file...
This might be why it works in development mode but not in production.
You can just do dynamic imports of modules or components (already compiled), no more to me. Take a look at your output compilation bundles, and try to find where are your components...
It turns out that I needed to specify the ROOT_URL correctly when initializing Meteor. With an ngrok http tunnel on port 3000 pointing to https://some-hash.ngrok.io, I had to start Meteor with: ROOT_URL="https://some-hash.ngrok.io" meteor. When I do this, I can access it fine and everything loads from my local IP and the ngrok URL, but I can't seem to load it up from localhost (it times out).
Specifying my local or public IP did not work, I could not get it to load through any of those methods.

node ts-jest spyOn method does not match overload

I'm trying to use Jest in conjunction with ts-jest to write unit tests for a nodeJS server. I have something set up very similar to below:
impl.ts
export const dependency = () => {}
index.ts
import { dependency } from './impl.ts';
export { dependency };
consumer.ts
import { dependency } from '../impl' <- importing from index.ts
export const consumer = () => {
try {
dependecy();
return true;
} catch (error) {
return false;
}
}
consumer.test.ts
import * as dependencies from '../impl'
import { consumer } from './consumer'
const mockDependency = jest.spyOn(dependencies, 'depenedncy');
describe('function consumer', function () {
beforeEach(function () {
mockDependency.mockReturnValueOnce(false);
});
test('should return true', () => {});
})
This is just toy code, but the actual export / import / test files follow a similar structure. I'm getting typescript errors along these lines:
TS2769: No overload matches this call.
Specifically, that the method being spied on is not part of the overload of the import for dependencies, so I can't stub it out. I am doing literally the same thing in a different test file and it has no issues. Anyone know how to resolve the typing issue?
The issue turned out to be in the typing of the dependency function itself. The return value typing was incorrect and that was what was resulting in the Typescript error. Essentially I had this:
export const dependency: Handler = () => {
return () => {} <- is of type Handler
}
rather than this
export const dependency = (): Handler => {
return () => {} <- is of type Handler
}
Stupid mistake, hope it helps someone else in the future. My take away is that if you have a type error that doesn't make sense make sure you check the typing of all variables involved.

ES6 import error handling

I am currently using Babel.
I did the following before with require:
try {
var myModule = require('my-module');
} catch (err) {
// send error to log file
}
However when trying to do this with import:
try {
import myModule from 'my-module';
} catch (err) {
// send error to log file
}
I get the error:
'import' and 'export' may only appear at the top level
Now I understand that import is different to require. From reading Are ES6 module imports hoisted? import hoists which means the imports are loaded before code execution.
What I did before was that if any requires failed a log was created which alerted me via email (sending logs to logstash etc.). So my question boils down to the following.
How does one handle import errors in a good practice fashion in nodejs? Does such a thing exist?
You can't catch static imports errors (cf. Boris' answer)
Yet, you could use a dynamic import() for that.
It's now supported by all evergreen browsers & Node, and is part of the standards since ES2020.
class ImportError extends Error {}
const loadModule = async (modulePath) => {
try {
return await import(modulePath)
} catch (e) {
throw new ImportError(`Unable to import module ${modulePath}`)
}
}
[2021 Edit] Look at Caveman answer for a more up to date answer allowing to make dynamic import
This talk give it away : https://github.com/ModuleLoader/es-module-loader/issues/280 and agree with what you said.
import only works at the base level. They are static and always load
before the module is run.
So you can't do a code check.
But, the good news is that as it's static, it can be analysed, tools like webpack throw errors at build time.
Supplementary dynamic import.
class ImportError extends Error {}
const loadModule = async (modulePath) => {
try {
return await import(modulePath)
} catch (e) {
throw new ImportError(`Unable to import module ${modulePath}`)
}
}
async function main() {
// import myDefault, {foo, bar} from '/modules/my-module.js'
const { default: myDefault, foo, bar } = await loadModule('/modules/my-module.js')
}
or chained_promises
import("/modules/my-module.js").then(module=>{
module.foo()
module.bar()
}).catch(err=>
console.log(err.message)
)
or Destructuring assignment
import("/modules/my-module.js").then(({foo, bar})=>{
foo()
bar()
}).catch(err=>
console.log(err.message)
)
A very modern answer to this now since cloud services is becoming the norm is to let the import fail and log to stderr as cloud services logs from stderr to their logging service. So basically you don't need to do anything.

Resources