Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ bundle.compat.js
bundle.native.js
wt
test/e2e/report
test/module-loading/test-projects
tmp
29 changes: 29 additions & 0 deletions .github/workflows/module-loading.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Module Loading Tests

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
module-loading:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Use Node.js 24.x
uses: actions/setup-node@v4
with:
node-version: 24.x
cache: "npm"
Comment on lines +16 to +20
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should run another node.js versions also: like "20" and "22"

If you checked it once and it works on 18/20/22 - its fine, we can keep "only node 24" check

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESM wouldn't work on node < v20.17.0, and only 24 has typescript types stripping turned on by default. I'll verify once again locally on different node versions, but going to ultimately keep the check turned on for 24 only


- name: Install dependencies
run: npm ci

- name: Build project
run: npm run build

- name: Run module loading tests
run: npm run test-module-loading
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"lint": "eslint --cache . && prettier --check .",
"reformat": "eslint --fix . && prettier --write .",
"prettier-watch": "onchange '**' --exclude-path .prettierignore -- prettier --write {{changed}}",
"test-unit": "_mocha \"test/!(integration)/**/*.js\"",
"test-unit": "_mocha \"test/!(integration|module-loading)/**/*.js\"",
"test-module-loading": "mocha test/module-loading/index.js",
"test": "npm run test-unit && npm run check-types && npm run lint",
"test-integration": "mocha -r ts-node/register -r test/integration/*/**",
"toc": "doctoc docs --title '### Contents'",
Expand Down
66 changes: 65 additions & 1 deletion src/test-reader/mocha-reader/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use strict";

const fs = require("fs");
const _ = require("lodash");
const Mocha = require("mocha");
const path = require("path");

const { MochaEventBus } = require("./mocha-event-bus");
const { TreeBuilderDecorator } = require("./tree-builder-decorator");
Expand All @@ -11,6 +13,8 @@ const { getMethodsByInterface } = require("./utils");
const logger = require("../../utils/logger");
const { enableSourceMaps } = require("../../utils/typescript");

const AMBIGUOUS_TYPESCRIPT_EXTENSIONS = new Set([".ts", ".tsx"]);

function getTagParser(original) {
return function (title, paramsOrFn, fn) {
if (typeof paramsOrFn === "function") {
Expand Down Expand Up @@ -59,7 +63,7 @@ async function readFiles(files, { esmDecorator, config, eventBus, runnableOpts,
files.forEach(f => mocha.addFile(f));

try {
await mocha.loadFilesAsync({ esmDecorator });
await loadMochaFiles(mocha, files, { esmDecorator });
} catch (err) {
const errorMessage = (err.message || "").split("\n")[0].trim();

Expand All @@ -77,6 +81,66 @@ async function readFiles(files, { esmDecorator, config, eventBus, runnableOpts,
applyOnly(mocha.suite, eventBus);
}

async function loadMochaFiles(mocha, files, { esmDecorator }) {
if (["true", "1"].includes(process.env.TESTPLANE_LOAD_FILES_ASYNC) || !files.some(shouldLoadWithRequire)) {
await mocha.loadFilesAsync({ esmDecorator });
return;
}

const originalFiles = mocha.files;
mocha.lazyLoadFiles(true);
Comment thread
KuznetsovRoman marked this conversation as resolved.

try {
for (const file of files) {
mocha.files = [file];

if (!shouldLoadWithRequire(file)) {
await mocha.loadFilesAsync({ esmDecorator });
continue;
}
Comment on lines +94 to +100
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't it written like that?

const loadWithRequireFiles = [];
const loadDefaultFiles = [];

for (const file of files) {
    if (shouldLoadWithRequire(file)) {
        loadWithRequireFiles.push(file)
    } else {
        loadDefaultFiles.push(file)
    }
}

// Load "loadDefaultFiles" with some kind of Promise.all
// Load "loadWithRequireFiles" with "mocha.loadFiles"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wouldn't gain anything from doing that: mocha itself loads files sequentially, here's the source of loadFilesAsync:

exports.loadFilesAsync = async (
  files,
  preLoadFunc,
  postLoadFunc,
  esmDecorator
) => {
  for (const file of files) {
    preLoadFunc(file);
    const result = await exports.requireOrImport(
      path.resolve(file),
      esmDecorator
    );
    postLoadFunc(file, result);
  }
};


try {
mocha.loadFiles();
} catch (err) {
if (err.code !== "ERR_REQUIRE_ESM" && err.code !== "ERR_REQUIRE_ASYNC_MODULE") {
throw err;
}

await mocha.loadFilesAsync({ esmDecorator });
}
}
} finally {
mocha.files = originalFiles;
}
}

function shouldLoadWithRequire(file) {
if (!process.features?.typescript || !AMBIGUOUS_TYPESCRIPT_EXTENSIONS.has(path.extname(file))) {
return false;
}

// Node.js 24 can import .ts files directly and warn before our require hook transpiles them.
return !isInsideEsmPackage(file);
}

function isInsideEsmPackage(file) {
let currentDir = path.dirname(path.resolve(file));
let parentDir;

while (currentDir !== parentDir) {
const packageJsonPath = path.join(currentDir, "package.json");

if (fs.existsSync(packageJsonPath)) {
return JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).type === "module";
}

parentDir = currentDir;
currentDir = path.dirname(currentDir);
}

return false;
}

function initBuildContext(outBus) {
outBus.emit(TestReaderEvents.NEW_BUILD_INSTRUCTION, ctx => {
ctx.treeBuilder = TreeBuilderDecorator.create(ctx.treeBuilder);
Expand Down
44 changes: 44 additions & 0 deletions test/module-loading/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use strict";

const path = require("path");
const execa = require("execa");

const TESTPLANE_BIN = path.resolve(__dirname, "../../bin/testplane");
const PROJECTS_DIR = path.resolve(__dirname, "test-projects");
const MODULE_TYPE_WARNING = "MODULE_TYPELESS_PACKAGE_JSON";

const projects = [
["js-cjs", "js-cjs test"],
["js-esm", "js-esm test"],
["ts-cjs", "ts-cjs test"],
["ts-esm", "ts-esm test"],
];

describe("module loading", () => {
projects.forEach(([projectName, testTitle]) => {
it(`reads ${projectName}`, async () => {
const { stdout, stderr } = await readTests(projectName);
const tests = JSON.parse(stdout);

assert.lengthOf(tests, 1);
assert.deepEqual(tests[0].titlePath, [projectName, testTitle]);
assert.deepEqual(tests[0].browserIds, ["chrome"]);
assert.isFalse(tests[0].pending);
assert.notInclude(stderr, MODULE_TYPE_WARNING);
});
});
});

function readTests(projectName) {
return execa(
process.execPath,
[TESTPLANE_BIN, "list-tests", "-c", "testplane.config.cjs", "tests", "--formatter", "list"],
{
cwd: path.join(PROJECTS_DIR, projectName),
env: {
...process.env,
NODE_OPTIONS: [process.env.NODE_OPTIONS, "--trace-warnings"].filter(Boolean).join(" "),
},
},
);
}
4 changes: 4 additions & 0 deletions test/module-loading/test-projects/js-cjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "module-loading-js-cjs",
"private": true
}
11 changes: 11 additions & 0 deletions test/module-loading/test-projects/js-cjs/testplane.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use strict";

module.exports = {
browsers: {
chrome: {
desiredCapabilities: {
browserName: "chrome",
},
},
},
};
8 changes: 8 additions & 0 deletions test/module-loading/test-projects/js-cjs/tests/basic.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use strict";

const assert = require("node:assert");
const { value } = require("./helpers/value");

describe("js-cjs", () => {
it("js-cjs test", () => assert.equal(value, "js-cjs-value"));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use strict";

module.exports = {
value: "js-cjs-value",
};
5 changes: 5 additions & 0 deletions test/module-loading/test-projects/js-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "module-loading-js-esm",
"private": true,
"type": "module"
}
11 changes: 11 additions & 0 deletions test/module-loading/test-projects/js-esm/testplane.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use strict";

module.exports = {
browsers: {
chrome: {
desiredCapabilities: {
browserName: "chrome",
},
},
},
};
6 changes: 6 additions & 0 deletions test/module-loading/test-projects/js-esm/tests/basic.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import assert from "node:assert";
import { value } from "./helpers/value.js";

describe("js-esm", () => {
it("js-esm test", () => assert.equal(value, "js-esm-value"));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = "js-esm-value";
5 changes: 5 additions & 0 deletions test/module-loading/test-projects/ts-cjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "module-loading-ts-cjs",
"private": true,
"type": "commonjs"
}
11 changes: 11 additions & 0 deletions test/module-loading/test-projects/ts-cjs/testplane.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use strict";

module.exports = {
browsers: {
chrome: {
desiredCapabilities: {
browserName: "chrome",
},
},
},
};
7 changes: 7 additions & 0 deletions test/module-loading/test-projects/ts-cjs/tests/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import assert from "node:assert";

const { value } = require("./helpers/value");

describe("ts-cjs", () => {
it("ts-cjs test", () => assert.equal(value, "ts-cjs-value"));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use strict";

module.exports = {
value: "ts-cjs-value",
};
5 changes: 5 additions & 0 deletions test/module-loading/test-projects/ts-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "module-loading-ts-esm",
"private": true,
"type": "module"
}
11 changes: 11 additions & 0 deletions test/module-loading/test-projects/ts-esm/testplane.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use strict";

module.exports = {
browsers: {
chrome: {
desiredCapabilities: {
browserName: "chrome",
},
},
},
};
6 changes: 6 additions & 0 deletions test/module-loading/test-projects/ts-esm/tests/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import assert from "node:assert";
import { value } from "./helpers/value.ts";

describe("ts-esm", () => {
it("ts-esm test", () => assert.equal(value, "ts-esm-value"));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = "ts-esm-value";
Loading
Loading