chore: refactor test (#19608)
Github Actions / lint (push) Has been cancelled Details
Github Actions / validate-legacy-node (push) Has been cancelled Details
Github Actions / benchmark (1/4) (push) Has been cancelled Details
Github Actions / benchmark (2/4) (push) Has been cancelled Details
Github Actions / benchmark (3/4) (push) Has been cancelled Details
Github Actions / benchmark (4/4) (push) Has been cancelled Details
Github Actions / basic (push) Has been cancelled Details
Github Actions / unit (push) Has been cancelled Details
Github Actions / integration (10.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (10.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (10.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (10.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (10.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (10.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (12.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (14.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (16.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (18.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (20.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (20.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (20.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (22.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (22.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (22.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (22.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (22.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (22.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (24.x, macos-latest, a) (push) Has been cancelled Details
Github Actions / integration (24.x, macos-latest, b) (push) Has been cancelled Details
Github Actions / integration (24.x, ubuntu-latest, a) (push) Has been cancelled Details
Github Actions / integration (24.x, ubuntu-latest, b) (push) Has been cancelled Details
Github Actions / integration (24.x, windows-latest, a) (push) Has been cancelled Details
Github Actions / integration (24.x, windows-latest, b) (push) Has been cancelled Details
Github Actions / integration (lts/*, ubuntu-latest, a, 1) (push) Has been cancelled Details
Github Actions / integration (lts/*, ubuntu-latest, b, 1) (push) Has been cancelled Details

This commit is contained in:
hai-x 2025-06-20 22:08:04 +08:00 committed by GitHub
parent 0af28df145
commit a55ac0ed6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 649 additions and 664 deletions

View File

@ -16,6 +16,7 @@ test/**/*.*
!test/**/infrastructure-log.js
!test/*.md
!test/helpers/*.*
!test/runner/**/*.*
!test/benchmarkCases/**/*.mjs
test/js/**/*.*

View File

@ -162,7 +162,7 @@
"fmt": "yarn fmt:base --log-level warn --write",
"fmt:check": "yarn fmt:base --check",
"fmt:base": "node node_modules/prettier/bin/prettier.cjs --cache --ignore-unknown .",
"fix": "yarn fix:code && fix:yarn && fix:special && yarn fmt",
"fix": "yarn fix:code && yarn fix:yarn && yarn fix:special && yarn fmt",
"fix:code": "yarn lint:code --fix",
"fix:yarn": "yarn-deduplicate -s highest yarn.lock",
"fix:special": "node node_modules/tooling/inherit-types --write && node node_modules/tooling/format-schemas --write && node tooling/generate-runtime-code.js --write && node tooling/generate-wasm-code.js --write && node node_modules/tooling/format-file-header --write && node node_modules/tooling/compile-to-definitions --write && node node_modules/tooling/precompile-schemas --write && node node_modules/tooling/generate-types --no-template-literals --write",

View File

@ -7,21 +7,15 @@ require("./helpers/warmup-webpack");
const path = require("path");
const fs = require("graceful-fs");
const vm = require("vm");
const url = require("url");
const { URL, pathToFileURL, fileURLToPath } = require("url");
const rimraf = require("rimraf");
const checkArrayExpectation = require("./checkArrayExpectation");
const createLazyTestEnv = require("./helpers/createLazyTestEnv");
const deprecationTracking = require("./helpers/deprecationTracking");
const FakeDocument = require("./helpers/FakeDocument");
const CurrentScript = require("./helpers/CurrentScript");
const prepareOptions = require("./helpers/prepareOptions");
const { parseResource } = require("../lib/util/identifier");
const captureStdio = require("./helpers/captureStdio");
const asModule = require("./helpers/asModule");
const filterInfraStructureErrors = require("./helpers/infrastructureLogErrors");
const { TestRunner } = require("./runner");
const casesPath = path.join(__dirname, "configCases");
const categories = fs.readdirSync(casesPath).map(cat => ({
@ -420,290 +414,35 @@ const describeCases = config => {
const bundlePath = testConfig.findBundle(i, optionsArr[i]);
if (bundlePath) {
filesCount++;
const document = new FakeDocument(outputDirectory);
const globalContext = {
console,
expect,
setTimeout,
clearTimeout,
document,
getComputedStyle:
document.getComputedStyle.bind(document),
location: {
href: "https://test.cases/path/index.html",
origin: "https://test.cases",
toString() {
return "https://test.cases/path/index.html";
}
}
};
const requireCache = Object.create(null);
const esmCache = new Map();
const esmIdentifier = `${category.name}-${testName}-${i}`;
const baseModuleScope = {
console,
const runner = new TestRunner({
target: options.target,
outputDirectory,
testMeta: {
category: category.name,
name: testName,
round: i
},
testConfig,
webpackOptions: options
});
runner.mergeModuleScope({
it: _it,
beforeEach: _beforeEach,
afterEach: _afterEach,
expect,
jest,
__STATS__: jsonStats,
__STATS_I__: i,
nsObj: m => {
Object.defineProperty(m, Symbol.toStringTag, {
value: "Module"
});
return m;
}
};
let runInNewContext = false;
if (
options.target === "web" ||
options.target === "webworker" ||
(Array.isArray(options.target) &&
(options.target.includes("web") ||
options.target.includes("webworker")))
) {
baseModuleScope.window = globalContext;
baseModuleScope.self = globalContext;
baseModuleScope.document = globalContext.document;
baseModuleScope.setTimeout = globalContext.setTimeout;
baseModuleScope.clearTimeout = globalContext.clearTimeout;
baseModuleScope.getComputedStyle =
globalContext.getComputedStyle;
baseModuleScope.URL = URL;
if (typeof Blob !== "undefined") {
baseModuleScope.Blob = Blob;
}
baseModuleScope.Worker =
require("./helpers/createFakeWorker")({
outputDirectory
});
runInNewContext = true;
}
if (testConfig.moduleScope) {
testConfig.moduleScope(baseModuleScope, options);
}
const esmContext = vm.createContext(baseModuleScope, {
name: "context for esm"
__STATS_I__: i
});
// eslint-disable-next-line no-loop-func
const _require = (
currentDirectory,
options,
module,
esmMode,
parentModule
) => {
if (testConfig.resolveModule) {
module = testConfig.resolveModule(module, i, options);
}
if (testConfig === undefined) {
throw new Error(
`_require(${module}) called after all tests from ${category.name} ${testName} have completed`
);
}
if (Array.isArray(module) || /^\.\.?\//.test(module)) {
let content;
let p;
let subPath = "";
if (Array.isArray(module)) {
p = path.join(currentDirectory, ".array-require.js");
content = `module.exports = (${module
.map(
arg => `require(${JSON.stringify(`./${arg}`)})`
)
.join(", ")});`;
} else {
p = path.join(currentDirectory, module);
content = fs.readFileSync(p, "utf-8");
const lastSlash = module.lastIndexOf("/");
let firstSlash = module.indexOf("/");
if (lastSlash !== -1 && firstSlash !== lastSlash) {
if (firstSlash !== -1) {
let next = module.indexOf("/", firstSlash + 1);
let dir = module.slice(firstSlash + 1, next);
while (dir === ".") {
firstSlash = next;
next = module.indexOf("/", firstSlash + 1);
dir = module.slice(firstSlash + 1, next);
}
}
subPath = module.slice(
firstSlash + 1,
lastSlash + 1
);
}
}
const isModule =
p.endsWith(".mjs") &&
options.experiments &&
options.experiments.outputModule;
if (isModule) {
if (!vm.SourceTextModule)
throw new Error(
"Running this test requires '--experimental-vm-modules'.\nRun with 'node --experimental-vm-modules node_modules/jest-cli/bin/jest'."
);
let esm = esmCache.get(p);
if (!esm) {
esm = new vm.SourceTextModule(content, {
identifier: `${esmIdentifier}-${p}`,
url: `${pathToFileURL(p).href}?${esmIdentifier}`,
context: esmContext,
initializeImportMeta: (meta, module) => {
meta.url = pathToFileURL(p).href;
},
importModuleDynamically: async (
specifier,
module
) => {
const normalizedSpecifier =
specifier.startsWith("file:")
? `./${path.relative(
path.dirname(p),
url.fileURLToPath(specifier)
)}`
: specifier.replace(
/https:\/\/example.com\/public\/path\//,
"./"
);
const result = await _require(
path.dirname(p),
options,
normalizedSpecifier,
"evaluated",
module
);
return await asModule(result, module.context);
}
});
esmCache.set(p, esm);
}
if (esmMode === "unlinked") return esm;
return (async () => {
if (esmMode === "unlinked") return esm;
await esm.link(
async (specifier, referencingModule) =>
await asModule(
await _require(
path.dirname(
referencingModule.identifier
? referencingModule.identifier.slice(
esmIdentifier.length + 1
)
: fileURLToPath(referencingModule.url)
),
options,
specifier,
"unlinked",
referencingModule
),
referencingModule.context,
true
)
);
// node.js 10 needs instantiate
if (esm.instantiate) esm.instantiate();
await esm.evaluate();
if (esmMode === "evaluated") return esm;
const ns = esm.namespace;
return ns.default && ns.default instanceof Promise
? ns.default
: ns;
})();
}
const isJSON = p.endsWith(".json");
if (isJSON) {
return JSON.parse(content);
}
if (p in requireCache) {
return requireCache[p].exports;
}
const m = {
exports: {}
};
requireCache[p] = m;
const moduleScope = {
...baseModuleScope,
require: _require.bind(
null,
path.dirname(p),
options
),
importScripts: url => {
expect(url).toMatch(
/^https:\/\/test\.cases\/path\//
);
_require(
outputDirectory,
options,
`.${url.slice("https://test.cases/path".length)}`
);
},
module: m,
exports: m.exports,
__dirname: path.dirname(p),
__filename: p,
_globalAssign: { expect }
};
if (testConfig.moduleScope) {
testConfig.moduleScope(moduleScope, options);
}
if (!runInNewContext)
content = `Object.assign(global, _globalAssign); ${content}`;
const args = Object.keys(moduleScope);
const argValues = args.map(arg => moduleScope[arg]);
const code = `(function(${args.join(
", "
)}) {${content}\n})`;
const oldCurrentScript = document.currentScript;
document.currentScript = new CurrentScript(subPath);
const fn = runInNewContext
? vm.runInNewContext(code, globalContext, p)
: vm.runInThisContext(code, p);
fn.call(
testConfig.nonEsmThis
? testConfig.nonEsmThis(module)
: m.exports,
...argValues
);
document.currentScript = oldCurrentScript;
return m.exports;
} else if (
testConfig.modules &&
module in testConfig.modules
) {
return testConfig.modules[module];
}
return require(
module.startsWith("node:") ? module.slice(5) : module
);
};
if (testConfig.moduleScope) {
testConfig.moduleScope(runner._moduleScope, options);
}
if (Array.isArray(bundlePath)) {
for (const bundlePathItem of bundlePath) {
results.push(
_require(
outputDirectory,
options,
`./${bundlePathItem}`
)
runner.require(outputDirectory, `./${bundlePathItem}`)
);
}
} else {
results.push(
_require(outputDirectory, options, bundlePath)
);
results.push(runner.require(outputDirectory, bundlePath));
}
}
}

View File

@ -4,11 +4,10 @@ require("./helpers/warmup-webpack");
const path = require("path");
const fs = require("graceful-fs");
const vm = require("vm");
const rimraf = require("rimraf");
const checkArrayExpectation = require("./checkArrayExpectation");
const createLazyTestEnv = require("./helpers/createLazyTestEnv");
const FakeDocument = require("./helpers/FakeDocument");
const { TestRunner } = require("./runner");
const casesPath = path.join(__dirname, "hotCases");
let categories = fs
@ -69,7 +68,11 @@ const describeCases = config => {
if (!options.output) options.output = {};
if (!options.output.path) options.output.path = outputDirectory;
if (!options.output.filename)
options.output.filename = "bundle.js";
options.output.filename = `bundle${
options.experiments && options.experiments.outputModule
? ".mjs"
: ".js"
}`;
if (!options.output.chunkFilename)
options.output.chunkFilename = "[name].chunk.[fullhash].js";
if (options.output.pathinfo === undefined)
@ -139,136 +142,20 @@ const describeCases = config => {
return;
}
const urlToPath = url => {
if (url.startsWith("https://test.cases/path/"))
url = url.slice(24);
return path.resolve(outputDirectory, `./${url}`);
};
const urlToRelativePath = url => {
if (url.startsWith("https://test.cases/path/"))
url = url.slice(24);
return `./${url}`;
};
const window = {
_elements: [],
fetch: async url => {
try {
const buffer = await new Promise((resolve, reject) => {
fs.readFile(urlToPath(url), (err, b) =>
err ? reject(err) : resolve(b)
);
});
return {
status: 200,
ok: true,
json: async () => JSON.parse(buffer.toString("utf-8"))
};
} catch (err) {
if (err.code === "ENOENT") {
return {
status: 404,
ok: false
};
}
throw err;
}
const runner = new TestRunner({
target: options.target,
outputDirectory,
testMeta: {
category: category.name,
name: testName,
env: "jsdom"
},
importScripts: url => {
expect(url).toMatch(/^https:\/\/test\.cases\/path\//);
_require(urlToRelativePath(url));
testConfig: {
...testConfig,
evaluateScriptOnAttached: true
},
document: {
createElement(type) {
const ele = {
_type: type,
getAttribute(name) {
return this[name];
},
setAttribute(name, value) {
this[name] = value;
},
removeAttribute(name) {
delete this[name];
},
parentNode: {
removeChild(node) {
window._elements = window._elements.filter(
item => item !== node
);
}
}
};
ele.sheet =
type === "link"
? new FakeDocument.FakeSheet(ele, outputDirectory)
: {};
return ele;
},
head: {
appendChild(element) {
window._elements.push(element);
if (element._type === "script") {
// run it
Promise.resolve().then(() => {
_require(urlToRelativePath(element.src));
});
} else if (element._type === "link") {
Promise.resolve().then(() => {
if (element.onload) {
// run it
element.onload({ type: "load" });
}
});
}
},
insertBefore(element, before) {
window._elements.push(element);
if (element._type === "script") {
// run it
Promise.resolve().then(() => {
_require(urlToRelativePath(element.src));
});
} else if (element._type === "link") {
// run it
Promise.resolve().then(() => {
element.onload({ type: "load" });
});
}
}
},
getElementsByTagName(name) {
if (name === "head") return [this.head];
if (name === "script" || name === "link") {
return window._elements.filter(
item => item._type === name
);
}
throw new Error("Not supported");
}
},
Worker: require("./helpers/createFakeWorker")({
outputDirectory
}),
EventSource: require("./helpers/EventSourceForNode"),
location: {
href: "https://test.cases/path/index.html",
origin: "https://test.cases",
toString() {
return "https://test.cases/path/index.html";
}
}
};
const moduleScope = {
window
};
if (testConfig.moduleScope) {
testConfig.moduleScope(moduleScope, options);
}
webpackOptions: options
});
function _next(callback) {
fakeUpdateLoaderOptions.updateIndex++;
@ -307,74 +194,31 @@ const describeCases = config => {
});
}
/**
* @private
* @param {string} module module
* @returns {EXPECTED_ANY} required module
*/
function _require(module) {
if (module.startsWith("./")) {
const p = path.join(outputDirectory, module);
if (module.endsWith(".css")) {
return fs.readFileSync(p, "utf-8");
}
if (module.endsWith(".json")) {
return JSON.parse(fs.readFileSync(p, "utf-8"));
}
const fn = vm.runInThisContext(
"(function(require, module, exports, __dirname, __filename, it, beforeEach, afterEach, expect, jest, self, window, fetch, document, importScripts, Worker, EventSource, NEXT, STATS) {" +
"global.expect = expect;" +
"global.it = it;" +
`function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; }${fs.readFileSync(
p,
"utf-8"
)}\n})`,
p
);
const m = {
exports: {}
};
fn.call(
m.exports,
_require,
m,
m.exports,
outputDirectory,
p,
_it,
_beforeEach,
_afterEach,
expect,
jest,
window,
window,
window.fetch,
window.document,
window.importScripts,
window.Worker,
window.EventSource,
_next,
jsonStats
);
return m.exports;
}
return require(module);
}
runner.mergeModuleScope({
it: _it,
beforeEach: _beforeEach,
afterEach: _afterEach,
STATE: jsonStats,
NEXT: _next
});
let promise = Promise.resolve();
const info = stats.toJson({ all: false, entrypoints: true });
if (config.target === "web") {
for (const file of info.entrypoints.main.assets) {
if (file.name.endsWith(".css")) {
const link = window.document.createElement("link");
link.href = path.join(outputDirectory, file.name);
window.document.head.appendChild(link);
const link =
runner._moduleScope.document.createElement("link");
link.href = file.name;
runner._moduleScope.document.head.appendChild(link);
} else {
_require(`./${file.name}`);
runner.require(outputDirectory, `./${file.name}`);
}
}
} else {
const assets = info.entrypoints.main.assets;
const result = _require(
const result = runner.require(
outputDirectory,
`./${assets[assets.length - 1].name}`
);
if (typeof result === "object" && "then" in result)

View File

@ -7,16 +7,13 @@ require("./helpers/warmup-webpack");
const path = require("path");
const fs = require("graceful-fs");
const vm = require("vm");
const rimraf = require("rimraf");
const { pathToFileURL, fileURLToPath } = require("url");
const checkArrayExpectation = require("./checkArrayExpectation");
const createLazyTestEnv = require("./helpers/createLazyTestEnv");
const { remove } = require("./helpers/remove");
const prepareOptions = require("./helpers/prepareOptions");
const deprecationTracking = require("./helpers/deprecationTracking");
const FakeDocument = require("./helpers/FakeDocument");
const asModule = require("./helpers/asModule");
const { TestRunner } = require("./runner");
/**
* @param {string} src src
@ -150,7 +147,11 @@ const describeCases = config => {
if (typeof options.output.pathinfo === "undefined")
options.output.pathinfo = true;
if (!options.output.filename)
options.output.filename = "bundle.js";
options.output.filename = `bundle${
options.experiments && options.experiments.outputModule
? ".mjs"
: ".js"
}`;
if (options.cache && options.cache.type === "filesystem") {
const cacheDirectory = path.join(tempDirectory, ".cache");
options.cache.cacheDirectory = cacheDirectory;
@ -269,178 +270,6 @@ const describeCases = config => {
)
return;
const globalContext = {
console,
expect,
setTimeout,
clearTimeout,
document: new FakeDocument()
};
const baseModuleScope = {
console,
it: run.it,
beforeEach: _beforeEach,
afterEach: _afterEach,
expect,
jest,
STATS_JSON: jsonStats,
nsObj: m => {
Object.defineProperty(m, Symbol.toStringTag, {
value: "Module"
});
return m;
},
window: globalContext,
self: globalContext,
WATCH_STEP: run.name,
STATE: state
};
const esmCache = new Map();
const esmIdentifier = `${category.name}-${testName}`;
const esmContext = vm.createContext(baseModuleScope, {
name: "context for esm"
});
// ESM
const isModule =
options.experiments && options.experiments.outputModule;
/**
* @param {string} currentDirectory The directory to resolve relative paths from
* @param {string} module The module to require
* @param {("unlinked"|"evaluated")} esmMode The mode for ESM module handling
* @returns {EXPECTED_ANY} required module
* @private
*/
function _require(currentDirectory, module, esmMode) {
if (/^\.\.?\//.test(module) || path.isAbsolute(module)) {
let fn;
const p = path.isAbsolute(module)
? module
: path.join(currentDirectory, module);
const content = fs.readFileSync(p, "utf-8");
if (isModule) {
if (!vm.SourceTextModule)
throw new Error(
"Running this test requires '--experimental-vm-modules'.\nRun with 'node --experimental-vm-modules node_modules/jest-cli/bin/jest'."
);
let esm = esmCache.get(p);
if (!esm) {
esm = new vm.SourceTextModule(content, {
identifier: `${esmIdentifier}-${p}`,
url: `${pathToFileURL(p).href}?${esmIdentifier}`,
context: esmContext,
initializeImportMeta: (meta, module) => {
meta.url = pathToFileURL(p).href;
},
importModuleDynamically: async (
specifier,
module
) => {
const normalizedSpecifier =
specifier.startsWith("file:")
? `./${path.relative(
path.dirname(p),
fileURLToPath(specifier)
)}`
: specifier.replace(
/https:\/\/test.cases\/path\//,
"./"
);
const result = await _require(
currentDirectory,
normalizedSpecifier,
"evaluated"
);
return await asModule(result, module.context);
}
});
esmCache.set(p, esm);
}
if (esmMode === "unlinked") return esm;
return (async () => {
if (esmMode === "unlinked") return esm;
await esm.link(
async (specifier, referencingModule) =>
await asModule(
await _require(
path.dirname(
referencingModule.identifier
? referencingModule.identifier.slice(
esmIdentifier.length + 1
)
: fileURLToPath(referencingModule.url)
),
specifier,
"unlinked"
),
referencingModule.context,
true
)
);
// node.js 10 needs instantiate
if (esm.instantiate) esm.instantiate();
await esm.evaluate();
if (esmMode === "evaluated") return esm;
const ns = esm.namespace;
return ns.default && ns.default instanceof Promise
? ns.default
: ns;
})();
}
if (
options.target === "web" ||
options.target === "webworker"
) {
fn = vm.runInNewContext(
"(function(require, module, exports, __dirname, __filename, it, WATCH_STEP, STATS_JSON, STATE, expect, window, self) {" +
`function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; }${
content
}\n})`,
globalContext,
p
);
} else {
fn = vm.runInThisContext(
"(function(require, module, exports, __dirname, __filename, it, WATCH_STEP, STATS_JSON, STATE, expect) {" +
"global.expect = expect;" +
`function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; }${
content
}\n})`,
p
);
}
const m = {
exports: {}
};
fn.call(
m.exports,
_require.bind(null, path.dirname(p)),
m,
m.exports,
path.dirname(p),
p,
run.it,
run.name,
jsonStats,
state,
expect,
globalContext,
globalContext
);
return module.exports;
} else if (
testConfig.modules &&
module in testConfig.modules
) {
return testConfig.modules[module];
}
return jest.requireActual(module);
}
let testConfig = {};
try {
// try to load a test file
@ -453,7 +282,28 @@ const describeCases = config => {
if (testConfig.noTests)
return process.nextTick(compilationFinished);
const runner = new TestRunner({
target: options.target,
outputDirectory,
testMeta: {
category: category.name,
name: testName,
env: "jsdom"
},
testConfig: {
...testConfig,
evaluateScriptOnAttached: true
},
webpackOptions: options
});
runner.mergeModuleScope({
it: run.it,
beforeEach: _beforeEach,
afterEach: _afterEach,
STATS_JSON: jsonStats,
STATE: state,
WATCH_STEP: run.name
});
const getBundle = (outputDirectory, module) => {
if (Array.isArray(module)) {
return module.map(arg =>
@ -475,7 +325,7 @@ const describeCases = config => {
)) {
promises.push(
Promise.resolve().then(() =>
_require(outputDirectory, p)
runner.require(outputDirectory, p)
)
);
}

View File

@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`ConfigTestCases css build-http exported tests should work with URLs in CSS 1`] = `
Array [

View File

@ -0,0 +1,8 @@
const path = require("path");
module.exports = {
// sharing global require cache between webpack.config.js and testing file
modules: {
[path.resolve(__dirname, "data.js")]: require("./data.js")
}
};

View File

@ -1,6 +1,13 @@
import "./style.css";
import fs from "fs";
import path from "path";
it("creates source maps for .css output files by default", function() {
var fs = require("fs");
var source = fs.readFileSync(__filename, "utf-8");
var match = /sourceMappingURL\s*=\s*(.*)\*\//.exec(source);
expect(match[1]).toBe("bundle0.css.map");
const css = fs.readFileSync(path.resolve(__dirname, "style.css"), "utf-8");
const map = JSON.parse(fs.readFileSync(path.resolve(__dirname, "style.css.map"), "utf-8"));
var match = /sourceMappingURL\s*=\s*(.*)\*\//.exec(css);
expect(match[1]).toBe("style.css.map");
expect(map).toHaveProperty("version", 3);
expect(map).toHaveProperty("file", "style.css");
});

View File

@ -0,0 +1,3 @@
body {
background-color: red;
}

View File

@ -1,3 +0,0 @@
var foo = {};
module.exports = foo;

View File

@ -1,12 +1,36 @@
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
output: {
filename: "bundle0.css"
filename: "bundle0.js"
},
node: {
__dirname: false,
__filename: false
},
devtool: "source-map"
devtool: "source-map",
module: {
rules: [
{
test: /\.css$/i,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: true
}
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "style.css",
chunkFilename: "[id].css"
})
]
};

View File

@ -14,6 +14,7 @@ module.exports = class EventSource {
url,
{
agent: false,
rejectUnauthorized: false,
headers: { accept: "text/event-stream" }
},
res => {

View File

@ -73,17 +73,34 @@ class FakeElement {
this.sheet = type === "link" ? new FakeSheet(this, basePath) : undefined;
}
appendChild(node) {
_attach(node) {
this._document._onElementAttached(node);
this._children.push(node);
node.parentNode = this;
}
_load(node) {
if (node._type === "link") {
setTimeout(() => {
if (node.onload) node.onload({ type: "load", target: node });
}, 100);
} else if (node._type === "script" && this._document.onScript) {
Promise.resolve().then(() => {
this._document.onScript(node.src);
});
}
}
insertBefore(node, before) {
this._attach(node);
this._load(node);
}
appendChild(node) {
this._attach(node);
this._load(node);
}
removeChild(node) {
const idx = this._children.indexOf(node);
if (idx >= 0) {

View File

@ -0,0 +1,10 @@
module.exports = {
moduleScope(scope, options) {
if (
(options.target === "web" || options.target === "webworker") &&
!scope.process
) {
scope.process = process;
}
}
};

View File

@ -16,7 +16,7 @@ it("should wait until promises returned by status handlers are fulfilled", (done
value = require("./file");
});
NEXT(require("../../update")(done, undefined, () => {
expect(handler.mock.calls).toStrictEqual([['check'], ['prepare'], ['dispose'], ['apply'], ['idle']]);
expect(handler.mock.calls).toEqual([['check'], ['prepare'], ['dispose'], ['apply'], ['idle']]);
for (let result of handler.mock.results)
expect(result.value.test).toHaveBeenCalledTimes(1);

484
test/runner/index.js Normal file
View File

@ -0,0 +1,484 @@
const fs = require("fs");
const path = require("path");
const vm = require("vm");
const { pathToFileURL, fileURLToPath } = require("url");
/**
* @param {string} path
* @returns {string}
*/
const getSubPath = path => {
let subPath = "";
const lastSlash = path.lastIndexOf("/");
let firstSlash = path.indexOf("/");
if (lastSlash !== -1 && firstSlash !== lastSlash) {
if (firstSlash !== -1) {
let next = path.indexOf("/", firstSlash + 1);
let dir = path.slice(firstSlash + 1, next);
while (dir === ".") {
firstSlash = next;
next = path.indexOf("/", firstSlash + 1);
dir = path.slice(firstSlash + 1, next);
}
}
subPath = path.slice(firstSlash + 1, lastSlash + 1);
}
return subPath;
};
/**
* @param {string} path
* @returns {boolean}
*/
const isRelativePath = path => /^\.\.?\//.test(path);
/**
* @param {string} url
* @param {string} outputDirectory
* @returns {string}
*/
const urlToPath = (url, outputDirectory) => {
if (url.startsWith("https://test.cases/path/")) url = url.slice(24);
else if (url.startsWith("https://test.cases/")) url = url.slice(19);
return path.resolve(outputDirectory, `./${url}`);
};
/**
* @param {string} url
* @returns {string}
*/
const urlToRelativePath = url => {
if (url.startsWith("https://test.cases/path/")) url = url.slice(24);
else if (url.startsWith("https://test.cases/")) url = url.slice(19);
return `./${url}`;
};
/**
* @typedef {Object} TestMeta
* @property {string} category
* @property {string} name
* @property {"jsdom"} [env]
* @property {number} [round]
*/
/**
* @typedef {Object} TestConfig
* @property {Function} [resolveModule]
* @property {Function} [moduleScope]
* @property {Function} [nonEsmThis]
* @property {boolean} [evaluateScriptOnAttached]
*/
/**
* @typedef {Object} TestRunnerOptions
* @property {string|string[]} target
* @property {string} outputDirectory
* @property {TestMeta} testMeta
* @property {TestConfig} testConfig
* @property {EXPECTED_ANY} webpackOptions
*/
/**
* @typedef {Object} ModuleInfo
* @property {string} subPath
* @property {string} modulePath
* @property {string} content
*/
/**
* @typedef {Object} RequireContext
* @property {"unlinked"|"evaluated"} esmMode
*/
/**
* @typedef {Object} ModuleRunner
* @property {(moduleInfo: ModuleInfo, context: RequireContext) => EXPECTED_ANY} cjs
* @property {(moduleInfo: ModuleInfo, context: RequireContext) => Promise<EXPECTED_ANY>} esm
* @property {(moduleInfo: ModuleInfo, context: RequireContext) => EXPECTED_ANY} json
* @property {(moduleInfo: ModuleInfo, context: RequireContext) => EXPECTED_ANY} raw
*/
class TestRunner {
/**
* @param {TestRunnerOptions} options
*/
constructor({
target,
outputDirectory,
testMeta,
testConfig,
webpackOptions
}) {
this.target = target;
this.outputDirectory = outputDirectory;
this.testConfig = testConfig || {};
this.testMeta = testMeta || {};
this.webpackOptions = webpackOptions || {};
this._runInNewContext = this.isTargetWeb();
this._globalContext = this.createBaseGlobalContext();
this._moduleScope = this.createBaseModuleScope();
this._moduleRunners = this.createModuleRunners();
}
/**
* @returns {ModuleRunner}
*/
createModuleRunners() {
return {
cjs: this.createCjsRunner(),
esm: this.createEsmRunner(),
json: this.createJSONRunner(),
raw: this.createRawRunner()
};
}
/**
* @returns {EXPECTED_ANY} globalContext
*/
createBaseGlobalContext() {
let base = { console, expect, setTimeout, clearTimeout };
Object.assign(base, this.setupEnv());
return base;
}
/**
* @returns {boolean}
*/
isTargetWeb() {
return (
this.target === "web" ||
this.target === "webworker" ||
(Array.isArray(this.target) &&
(this.target.includes("web") || this.target.includes("webworker")))
);
}
/**
* @returns {boolean}
*/
jsDom() {
return this.testMeta.env === "jsdom" || this.isTargetWeb();
}
/**
* @returns {EXPECTED_ANY} moduleScope
*/
createBaseModuleScope() {
let base = {
console,
expect,
jest,
nsObj: m => {
Object.defineProperty(m, Symbol.toStringTag, {
value: "Module"
});
return m;
}
};
if (this.jsDom()) {
Object.assign(base, this._globalContext);
base.window = this._globalContext;
base.self = this._globalContext;
}
return base;
}
/**
* @param {EXPECTED_ANY} globalContext
* @returns {EXPECTED_ANY}
*/
mergeGlobalContext(globalContext) {
return Object.assign(this._globalContext, globalContext);
}
/**
* @param {EXPECTED_ANY} moduleScope
* @returns {EXPECTED_ANY}
*/
mergeModuleScope(moduleScope) {
return Object.assign(this._moduleScope, moduleScope);
}
/**
* @param {string} currentDirectory
* @param {string|string[]} module
* @returns {ModuleInfo}
*/
_resolveModule(currentDirectory, module) {
if (Array.isArray(module)) {
return {
subPath: "",
modulePath: path.join(currentDirectory, ".array-require.js"),
content: `module.exports = (${module
.map(arg => `require(${JSON.stringify(`./${arg}`)})`)
.join(", ")});`
};
}
if (isRelativePath(module)) {
return {
subPath: getSubPath(module),
modulePath: path.join(currentDirectory, module),
content: fs.readFileSync(path.join(currentDirectory, module), "utf-8")
};
}
if (path.isAbsolute(module)) {
return {
subPath: "",
modulePath: module,
content: fs.readFileSync(module, "utf-8")
};
}
}
/**
* @param {string} currentDirectory
* @param {string|string[]} module
* @param {RequireContext} [context={}]
* @returns {EXPECTED_ANY}
*/
require(currentDirectory, module, context = {}) {
if (this.testConfig.modules && module in this.testConfig.modules) {
return this.testConfig.modules[module];
}
if (this.testConfig.resolveModule) {
module = this.testConfig.resolveModule(
module,
this.testMeta.round || 0,
this.webpackOptions
);
}
let moduleInfo = this._resolveModule(currentDirectory, module);
if (!moduleInfo) {
return require(module.startsWith("node:") ? module.slice(5) : module);
}
const { modulePath } = moduleInfo;
if (
modulePath.endsWith(".mjs") &&
this.webpackOptions.experiments &&
this.webpackOptions.experiments.outputModule
) {
return this._moduleRunners.esm(moduleInfo, context);
}
if (modulePath.endsWith(".json")) {
return this._moduleRunners.json(moduleInfo, context);
}
if (["css"].includes(modulePath.split(".").pop())) {
return this._moduleRunners.raw(moduleInfo, context);
}
return this._moduleRunners.cjs(moduleInfo, context);
}
/**
* @returns {(moduleInfo: ModuleInfo, context: RequireContext) => EXPECTED_ANY}
*/
createCjsRunner() {
const requireCache = Object.create(null);
return (moduleInfo, context) => {
const { modulePath, subPath, content } = moduleInfo;
let _content = content;
if (modulePath in requireCache) {
return requireCache[modulePath].exports;
}
const mod = {
exports: {}
};
requireCache[modulePath] = mod;
const moduleScope = {
...this._moduleScope,
require: this.require.bind(this, path.dirname(modulePath)),
importScripts: url => {
expect(url).toMatch(/^https:\/\/test\.cases\/path\//);
this.require(this.outputDirectory, urlToRelativePath(url));
},
module: mod,
exports: mod.exports,
__dirname: path.dirname(modulePath),
__filename: modulePath,
_globalAssign: { expect, it: this._moduleScope.it }
};
// Call again because some tests rely on `scope.module`
if (this.testConfig.moduleScope) {
this.testConfig.moduleScope(moduleScope, this.webpackOptions);
}
if (!this._runInNewContext)
_content = `Object.assign(global, _globalAssign); ${content}`;
const args = Object.keys(moduleScope);
const argValues = args.map(arg => moduleScope[arg]);
const code = `(function(${args.join(", ")}) {${_content}\n})`;
const document = this._moduleScope.document;
const fn = this._runInNewContext
? vm.runInNewContext(code, this._globalContext, modulePath)
: vm.runInThisContext(code, modulePath);
const call = () => {
fn.call(
this.testConfig.nonEsmThis
? this.testConfig.nonEsmThis(module)
: mod.exports,
...argValues
);
};
if (document) {
const CurrentScript = require("../helpers/CurrentScript");
const oldCurrentScript = document.currentScript;
document.currentScript = new CurrentScript(subPath);
try {
call();
} finally {
document.currentScript = oldCurrentScript;
}
} else {
call();
}
return mod.exports;
};
}
/**
* @returns {(moduleInfo: ModuleInfo, context: RequireContext) => Promise<EXPECTED_ANY>}
*/
createEsmRunner() {
const esmCache = new Map();
const { category, name, round } = this.testMeta;
const esmIdentifier = `${category.name}-${name}-${round || 0}`;
let esmContext = null;
return (moduleInfo, context) => {
const asModule = require("../helpers/asModule");
// lazy bind esm context
if (!esmContext) {
esmContext = vm.createContext(this._moduleScope, {
name: "context for esm"
});
}
const { modulePath, subPath, content } = moduleInfo;
const { esmMode } = context;
if (!vm.SourceTextModule)
throw new Error(
"Running this test requires '--experimental-vm-modules'.\nRun with 'node --experimental-vm-modules node_modules/jest-cli/bin/jest'."
);
let esm = esmCache.get(modulePath);
if (!esm) {
esm = new vm.SourceTextModule(content, {
identifier: `${esmIdentifier}-${modulePath}`,
url: `${pathToFileURL(modulePath).href}?${esmIdentifier}`,
context: esmContext,
initializeImportMeta: (meta, module) => {
meta.url = pathToFileURL(modulePath).href;
},
importModuleDynamically: async (specifier, module) => {
const normalizedSpecifier = specifier.startsWith("file:")
? `./${path.relative(
path.dirname(modulePath),
fileURLToPath(specifier)
)}`
: specifier.replace(
/https:\/\/example.com\/public\/path\//,
"./"
);
const result = await this.require(
path.dirname(modulePath),
normalizedSpecifier,
{
esmMode: "evaluated"
}
);
return await asModule(result, module.context);
}
});
esmCache.set(modulePath, esm);
}
if (esmMode === "unlinked") return esm;
return (async () => {
if (esmMode === "unlinked") return esm;
await esm.link(
async (specifier, referencingModule) =>
await asModule(
await this.require(
path.dirname(
referencingModule.identifier
? referencingModule.identifier.slice(
esmIdentifier.length + 1
)
: fileURLToPath(referencingModule.url)
),
specifier,
{ esmMode: "unlinked" }
),
referencingModule.context,
true
)
);
// node.js 10 needs instantiate
if (esm.instantiate) esm.instantiate();
await esm.evaluate();
if (esmMode === "evaluated") return esm;
const ns = esm.namespace;
return ns.default && ns.default instanceof Promise ? ns.default : ns;
})();
};
}
createJSONRunner() {
return moduleInfo => {
return JSON.parse(moduleInfo.content);
};
}
createRawRunner() {
return moduleInfo => {
return moduleInfo.content;
};
}
setupEnv() {
if (this.jsDom()) {
const outputDirectory = this.outputDirectory;
const FakeDocument = require("../helpers/FakeDocument");
const createFakeWorker = require("../helpers/createFakeWorker");
const EventSource = require("../helpers/EventSourceForNode");
const document = new FakeDocument(outputDirectory);
if (this.testConfig.evaluateScriptOnAttached) {
document.onScript = src => {
this.require(outputDirectory, urlToRelativePath(src));
};
}
const fetch = async url => {
try {
const buffer = await new Promise((resolve, reject) => {
fs.readFile(urlToPath(url, this.outputDirectory), (err, b) =>
err ? reject(err) : resolve(b)
);
});
return {
status: 200,
ok: true,
json: async () => JSON.parse(buffer.toString("utf-8"))
};
} catch (err) {
if (err.code === "ENOENT") {
return {
status: 404,
ok: false
};
}
throw err;
}
};
let env = {
setTimeout,
document,
location: {
href: "https://test.cases/path/index.html",
origin: "https://test.cases",
toString() {
return "https://test.cases/path/index.html";
}
},
getComputedStyle: document.getComputedStyle.bind(document),
Worker: createFakeWorker({
outputDirectory
}),
URL,
EventSource,
clearTimeout,
fetch
};
if (typeof Blob !== "undefined") {
// node.js >= 18
env.Blob = Blob;
}
return env;
}
return {};
}
}
module.exports.TestRunner = TestRunner;

View File

@ -25,7 +25,7 @@ module.exports = {
}
},
output: {
filename: "[name].[contenthash].js",
chunkFilename: "[name].[contenthash].js"
filename: "[name].[contenthash].mjs",
chunkFilename: "[name].[contenthash].mjs"
}
};

View File

@ -129,7 +129,7 @@
"@babel/template" "^7.27.2"
"@babel/types" "^7.27.3"
"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.27.3", "@babel/parser@^7.27.4", "@babel/parser@^7.27.5", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.27.4", "@babel/parser@^7.27.5", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
version "7.27.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.5.tgz#ed22f871f110aa285a6fd934a0efed621d118826"
integrity sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==