Summary: I have an async function that fails when it is imported from the node_modules directory but succeeds when it is imported from my source directory. I've tested this by just copy / pasting code into the node_modules directory.
I had always assumed that importing code from node_modules was exactly the same as importing from any other directory, but apparently this isn't true.
Question: Why does the below function clickBodyTwice work when imported from my source directory but not when imported from node_modules?
Disclaimer: The function uses the TestCafe library and makes use of implicit test controller access. They clearly say this is not supported in an async context, and the fix is provided. My question is not how to fix this, but why there is a difference between importing from the two directories.
Background: I am attempting to export a library that uses this pattern. The devs have used it across the entire project for years. Surprisingly, it works even though TestCafe explicitly says it shouldn't. That is until you export the library and import it into node_modules. The fix is to pass the test controller as a parameter, but unfortunately that's going to require a lot of refactoring, and educating devs why not to use this pattern.
click-body-twice.js
const { t } = require("testcafe");
async function clickBodyTwice() {
console.log("begin");
await t.click("body");
console.log("clicked once");
await t.click("body");
console.log("clicked twice");
}
exports.clickBodyTwice = clickBodyTwice;
Here I import this function from two different places, one succeeds and one fails. The test just navigates to google and attempts to click the body twice.
test.js
const { clickBodyTwice: topLevelFn } = require("./@@bug-repro/click-body-twice")
const { clickBodyTwice: nodeModuleFn } = require("./node_modules/@@bug-repro/click-body-twice")
fixture("Testcafe bug repro")
test("succeeds", async (t) => {
await t.navigateTo("http://www.google.com");
await topLevelFn();
})
test("fails", async (t) => {
await t.navigateTo("http://www.google.com");
await nodeModuleFn();
})
package.json
{
"dependencies": {
"testcafe": "3.5.0"
}
}
To reproduce:
I reproduced with Node v20.11.0 & v18.19.0
- Copy the above code into the following file structure:
@@bug-repro
- click-body-twice.js
package.json
test.js
- Run
npm i - Copy
@@bug-reprointonode_modules - Run
npx testcafe chrome .\test.js
The result:
PS C:\repos\testcafe-bug> npx testcafe chrome .\test.js
Running tests in:
- Chrome 120.0.0.0 / Windows 10
Testcafe bug repro
begin
clicked once
clicked twice
√ succeeds
begin
clicked once
× fails
1) The action does not have implicit test controller access. Reference the 't' object to gain it.
Browser: Chrome 120.0.0.0 / Windows 10
3 | console.log("begin");
4 |
5 | await t.click("body");
6 | console.log("clicked once");
7 |
> 8 | await t.click("body");
9 | console.log("clicked twice");
10 |}
11 |exports.clickBodyTwice = clickBodyTwice;
12 |
at clickBodyTwice (C:\repos\testcafe-bug\node_modules\@@bug-repro\click-body-twice.js:8:10)
1/2 failed (7s)
Notice how the first click attempt does have access to the test controller, while the second does not. Also notice that the exact same code imported from somewhere else does not face the same issue. This leads me to believe that await acts differently when in the node_modules directory, and somehow blocks TestCafe from referencing the test controller. I'm not sure the mechanism TestCafe uses to get implicit test controller access, nor do I know why Node.js would act differently when importing code from node_modules. Any information would be great.
I've also reported this as a bug in the hopes I'll get an explanation: https://github.com/DevExpress/testcafe/issues/8121
Update
Step through debugging reveals TestCafe is using V8's stacktrace API via the callsite package to find the test controller.
I've stopped execution at the same point where the failure occurs, and the stack traces are different.
They've actually embedded the controller ID into a function name, and retrieve it via the stack trace. This mechanism seems to fail in the node_modules directory.

