diff --git a/.prettierignore b/.prettierignore index ebf1141fc..0d57d8051 100644 --- a/.prettierignore +++ b/.prettierignore @@ -16,6 +16,7 @@ test/**/*.* !test/**/infrastructure-log.js !test/*.md !test/helpers/*.* +!test/runner/**/*.* !test/benchmarkCases/**/*.mjs test/js/**/*.* diff --git a/package.json b/package.json index e39f3cf1d..d1689b0b6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/ConfigTestCases.template.js b/test/ConfigTestCases.template.js index b9193d7e8..0517ef716 100644 --- a/test/ConfigTestCases.template.js +++ b/test/ConfigTestCases.template.js @@ -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)); } } } diff --git a/test/HotTestCases.template.js b/test/HotTestCases.template.js index d0e14a17f..7b0d31653 100644 --- a/test/HotTestCases.template.js +++ b/test/HotTestCases.template.js @@ -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) diff --git a/test/WatchTestCases.template.js b/test/WatchTestCases.template.js index 194b8b03e..6b0b9a160 100644 --- a/test/WatchTestCases.template.js +++ b/test/WatchTestCases.template.js @@ -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) ) ); } diff --git a/test/__snapshots__/ConfigTestCases.basictest.js.snap b/test/__snapshots__/ConfigTestCases.basictest.js.snap index 23153134e..ee01f31ff 100644 --- a/test/__snapshots__/ConfigTestCases.basictest.js.snap +++ b/test/__snapshots__/ConfigTestCases.basictest.js.snap @@ -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 [ diff --git a/test/configCases/plugins/progress-plugin/test.config.js b/test/configCases/plugins/progress-plugin/test.config.js new file mode 100644 index 000000000..67135d9eb --- /dev/null +++ b/test/configCases/plugins/progress-plugin/test.config.js @@ -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") + } +}; diff --git a/test/configCases/source-map/default-filename-extensions-css/index.js b/test/configCases/source-map/default-filename-extensions-css/index.js index 55d51278f..12cac76ec 100644 --- a/test/configCases/source-map/default-filename-extensions-css/index.js +++ b/test/configCases/source-map/default-filename-extensions-css/index.js @@ -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"); }); \ No newline at end of file diff --git a/test/configCases/source-map/default-filename-extensions-css/style.css b/test/configCases/source-map/default-filename-extensions-css/style.css new file mode 100644 index 000000000..40363d3c1 --- /dev/null +++ b/test/configCases/source-map/default-filename-extensions-css/style.css @@ -0,0 +1,3 @@ +body { + background-color: red; +} \ No newline at end of file diff --git a/test/configCases/source-map/default-filename-extensions-css/test.js b/test/configCases/source-map/default-filename-extensions-css/test.js deleted file mode 100644 index d336df4c8..000000000 --- a/test/configCases/source-map/default-filename-extensions-css/test.js +++ /dev/null @@ -1,3 +0,0 @@ -var foo = {}; - -module.exports = foo; \ No newline at end of file diff --git a/test/configCases/source-map/default-filename-extensions-css/webpack.config.js b/test/configCases/source-map/default-filename-extensions-css/webpack.config.js index ae476c291..29cefe96e 100644 --- a/test/configCases/source-map/default-filename-extensions-css/webpack.config.js +++ b/test/configCases/source-map/default-filename-extensions-css/webpack.config.js @@ -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" + }) + ] }; diff --git a/test/helpers/EventSourceForNode.js b/test/helpers/EventSourceForNode.js index ae9437871..17f30aad6 100644 --- a/test/helpers/EventSourceForNode.js +++ b/test/helpers/EventSourceForNode.js @@ -14,6 +14,7 @@ module.exports = class EventSource { url, { agent: false, + rejectUnauthorized: false, headers: { accept: "text/event-stream" } }, res => { diff --git a/test/helpers/FakeDocument.js b/test/helpers/FakeDocument.js index 700be0d13..fe02264c0 100644 --- a/test/helpers/FakeDocument.js +++ b/test/helpers/FakeDocument.js @@ -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) { diff --git a/test/hotCases/lazy-compilation/https/test.config.js b/test/hotCases/lazy-compilation/https/test.config.js new file mode 100644 index 000000000..afaea3417 --- /dev/null +++ b/test/hotCases/lazy-compilation/https/test.config.js @@ -0,0 +1,10 @@ +module.exports = { + moduleScope(scope, options) { + if ( + (options.target === "web" || options.target === "webworker") && + !scope.process + ) { + scope.process = process; + } + } +}; diff --git a/test/hotCases/status/accept/index.js b/test/hotCases/status/accept/index.js index 597a2bd0e..95c4c6616 100644 --- a/test/hotCases/status/accept/index.js +++ b/test/hotCases/status/accept/index.js @@ -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); diff --git a/test/runner/index.js b/test/runner/index.js new file mode 100644 index 000000000..35cc0d8b2 --- /dev/null +++ b/test/runner/index.js @@ -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} 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} + */ + 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; diff --git a/test/watchCases/chunks/esm-async-chunks-hmr/webpack.config.js b/test/watchCases/chunks/esm-async-chunks-hmr/webpack.config.js index eb4831828..7957917ec 100644 --- a/test/watchCases/chunks/esm-async-chunks-hmr/webpack.config.js +++ b/test/watchCases/chunks/esm-async-chunks-hmr/webpack.config.js @@ -25,7 +25,7 @@ module.exports = { } }, output: { - filename: "[name].[contenthash].js", - chunkFilename: "[name].[contenthash].js" + filename: "[name].[contenthash].mjs", + chunkFilename: "[name].[contenthash].mjs" } }; diff --git a/yarn.lock b/yarn.lock index 9003a9dd4..ab72adca4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==