webpack/test/ConfigTestCases.template.js

530 lines
15 KiB
JavaScript
Raw Normal View History

"use strict";
require("./helpers/warmup-webpack");
2025-04-16 22:04:11 +08:00
/** @typedef {Record<string, EXPECTED_ANY>} Env */
/** @typedef {{ testPath: string } TestOptions */
const path = require("path");
const fs = require("graceful-fs");
const rimraf = require("rimraf");
2025-07-03 17:06:45 +08:00
const { parseResource } = require("../lib/util/identifier");
const checkArrayExpectation = require("./checkArrayExpectation");
2025-07-03 17:06:45 +08:00
const captureStdio = require("./helpers/captureStdio");
const createLazyTestEnv = require("./helpers/createLazyTestEnv");
const deprecationTracking = require("./helpers/deprecationTracking");
2022-02-15 02:31:59 +08:00
const filterInfraStructureErrors = require("./helpers/infrastructureLogErrors");
2025-07-03 17:06:45 +08:00
const prepareOptions = require("./helpers/prepareOptions");
2025-06-20 22:08:04 +08:00
const { TestRunner } = require("./runner");
2022-02-10 17:41:48 +08:00
const casesPath = path.join(__dirname, "configCases");
2024-07-31 11:31:11 +08:00
const categories = fs.readdirSync(casesPath).map(cat => ({
name: cat,
tests: fs
.readdirSync(path.join(casesPath, cat))
.filter(folder => !folder.startsWith("_"))
.sort()
}));
2024-07-31 11:31:11 +08:00
const createLogger = appendTarget => ({
log: l => appendTarget.push(l),
debug: l => appendTarget.push(l),
trace: l => appendTarget.push(l),
info: l => appendTarget.push(l),
warn: console.warn.bind(console),
error: console.error.bind(console),
logTime: () => {},
group: () => {},
groupCollapsed: () => {},
groupEnd: () => {},
profile: () => {},
profileEnd: () => {},
clear: () => {},
status: () => {}
});
2022-02-10 17:41:48 +08:00
const describeCases = config => {
describe(config.name, () => {
let stderr;
beforeEach(() => {
stderr = captureStdio(process.stderr, true);
});
afterEach(() => {
stderr.restore();
});
jest.setTimeout(20000);
for (const category of categories) {
2024-07-31 13:38:50 +08:00
// eslint-disable-next-line no-loop-func
describe(category.name, () => {
for (const testName of category.tests) {
2024-07-31 13:38:50 +08:00
// eslint-disable-next-line no-loop-func
describe(testName, () => {
const testDirectory = path.join(casesPath, category.name, testName);
const filterPath = path.join(testDirectory, "test.filter.js");
2024-05-08 17:43:56 +08:00
if (fs.existsSync(filterPath) && !require(filterPath)(config)) {
2024-06-11 20:32:02 +08:00
// eslint-disable-next-line jest/no-disabled-tests
describe.skip(testName, () => {
it("filtered", () => {});
});
return;
}
2022-02-10 17:41:48 +08:00
const infraStructureLog = [];
const outBaseDir = path.join(__dirname, "js");
const testSubPath = path.join(config.name, category.name, testName);
const outputDirectory = path.join(outBaseDir, testSubPath);
const cacheDirectory = path.join(outBaseDir, ".cache", testSubPath);
2024-07-31 09:56:53 +08:00
let options;
let optionsArr;
let testConfig;
beforeAll(() => {
options = prepareOptions(
require(path.join(testDirectory, "webpack.config.js")),
{ testPath: outputDirectory }
);
2025-07-03 17:06:45 +08:00
optionsArr = [...(Array.isArray(options) ? options : [options])];
2024-08-02 02:36:27 +08:00
for (const [idx, options] of optionsArr.entries()) {
if (!options.context) options.context = testDirectory;
if (!options.mode) options.mode = "production";
if (!options.optimization) options.optimization = {};
if (options.optimization.minimize === undefined) {
options.optimization.minimize = false;
}
if (options.optimization.minimizer === undefined) {
options.optimization.minimizer = [
new (require("terser-webpack-plugin"))({
parallel: false
})
];
}
if (!options.entry) options.entry = "./index.js";
if (!options.target) options.target = "async-node";
if (!options.output) options.output = {};
if (!options.output.path) options.output.path = outputDirectory;
if (typeof options.output.pathinfo === "undefined") {
options.output.pathinfo = true;
}
if (!options.output.filename) {
2024-07-31 10:39:30 +08:00
options.output.filename = `bundle${idx}${
options.experiments && options.experiments.outputModule
? ".mjs"
2024-07-31 10:39:30 +08:00
: ".js"
}`;
}
2024-05-08 00:38:06 +08:00
if (config.cache) {
options.cache = {
cacheDirectory,
name:
options.cache && options.cache !== true
? options.cache.name
: `config-${idx}`,
2024-05-08 00:38:06 +08:00
...config.cache
};
}
if (config.cache) {
2022-02-10 17:41:48 +08:00
options.infrastructureLogging = {
debug: true,
console: createLogger(infraStructureLog)
};
}
if (!options.snapshot) options.snapshot = {};
if (!options.snapshot.managedPaths) {
options.snapshot.managedPaths = [
path.resolve(__dirname, "../node_modules")
];
}
2024-08-02 02:36:27 +08:00
}
testConfig = {
2025-04-22 19:09:25 +08:00
findBundle(i, options) {
const ext = path.extname(
parseResource(options.output.filename).path
);
if (
fs.existsSync(
2024-07-31 10:39:30 +08:00
path.join(options.output.path, `bundle${i}${ext}`)
)
) {
2024-07-31 10:39:30 +08:00
return `./bundle${i}${ext}`;
}
},
timeout: 30000
};
try {
// try to load a test file
testConfig = Object.assign(
testConfig,
require(path.join(testDirectory, "test.config.js"))
);
2024-07-31 15:37:05 +08:00
} catch (_err) {
// ignored
}
if (testConfig.timeout) setDefaultTimeout(testConfig.timeout);
});
// eslint-disable-next-line jest/no-duplicate-hooks
beforeAll(() => {
rimraf.sync(cacheDirectory);
});
afterAll(() => {
// cleanup
options = undefined;
optionsArr = undefined;
testConfig = undefined;
});
const handleFatalError = (err, done) => {
const fakeStats = {
errors: [
{
message: err.message,
stack: err.stack
}
]
};
if (
checkArrayExpectation(
testDirectory,
fakeStats,
"error",
"Error",
2024-11-01 21:55:49 +08:00
options,
done
)
) {
return;
}
// Wait for uncaught errors to occur
setTimeout(done, 200);
};
if (config.cache) {
it(`${testName} should pre-compile to fill disk cache (1st)`, done => {
rimraf.sync(outputDirectory);
fs.mkdirSync(outputDirectory, { recursive: true });
2022-02-10 17:41:48 +08:00
infraStructureLog.length = 0;
const deprecationTracker = deprecationTracking.start();
2024-06-20 18:55:18 +08:00
const compiler = require("..")(options);
2024-06-20 18:55:18 +08:00
compiler.run(err => {
deprecationTracker();
2024-06-20 18:55:18 +08:00
if (err) return handleFatalError(err, done);
const infrastructureLogging = stderr.toString();
if (infrastructureLogging) {
return done(
new Error(
2024-07-31 10:39:30 +08:00
`Errors/Warnings during build:\n${
infrastructureLogging
2024-07-31 10:39:30 +08:00
}`
)
);
}
2022-02-15 02:31:59 +08:00
const infrastructureLogErrors = filterInfraStructureErrors(
infraStructureLog,
{
run: 1,
options
}
);
if (
infrastructureLogErrors.length &&
checkArrayExpectation(
testDirectory,
{ infrastructureLogs: infrastructureLogErrors },
"infrastructureLog",
"infrastructure-log",
"InfrastructureLog",
2024-11-01 21:55:49 +08:00
options,
2022-02-15 02:31:59 +08:00
done
)
) {
return;
2022-02-10 17:41:48 +08:00
}
2024-06-20 18:55:18 +08:00
compiler.close(closeErr => {
2024-06-21 00:05:40 +08:00
if (closeErr) return handleFatalError(closeErr, done);
2024-06-20 18:55:18 +08:00
done();
});
});
}, 60000);
it(`${testName} should pre-compile to fill disk cache (2nd)`, done => {
rimraf.sync(outputDirectory);
fs.mkdirSync(outputDirectory, { recursive: true });
2022-02-10 17:41:48 +08:00
infraStructureLog.length = 0;
const deprecationTracker = deprecationTracking.start();
2024-06-20 18:55:18 +08:00
const compiler = require("..")(options);
2024-06-20 18:55:18 +08:00
compiler.run((err, stats) => {
deprecationTracker();
if (err) return handleFatalError(err, done);
const { modules, children, errorsCount } = stats.toJson({
all: false,
modules: true,
errorsCount: true
});
if (errorsCount === 0) {
const infrastructureLogging = stderr.toString();
if (infrastructureLogging) {
return done(
new Error(
2024-07-31 10:39:30 +08:00
`Errors/Warnings during build:\n${
infrastructureLogging
2024-07-31 10:39:30 +08:00
}`
)
);
}
const allModules = children
? children.reduce(
2025-07-03 17:06:45 +08:00
(all, { modules }) => [...all, ...modules],
modules || []
2024-07-31 05:43:19 +08:00
)
: modules;
if (
allModules.some(
m => m.type !== "cached modules" && !m.cached
)
) {
return done(
new Error(
`Some modules were not cached:\n${stats.toString({
all: false,
modules: true,
modulesSpace: 100
})}`
)
);
}
}
2022-02-15 02:31:59 +08:00
const infrastructureLogErrors = filterInfraStructureErrors(
infraStructureLog,
{
run: 2,
options
}
);
if (
infrastructureLogErrors.length &&
checkArrayExpectation(
testDirectory,
{ infrastructureLogs: infrastructureLogErrors },
"infrastructureLog",
"infrastructure-log",
"InfrastructureLog",
2024-11-01 21:55:49 +08:00
options,
2022-02-15 02:31:59 +08:00
done
)
) {
return;
2022-02-10 17:41:48 +08:00
}
2024-06-20 18:55:18 +08:00
compiler.close(closeErr => {
2024-06-21 00:05:40 +08:00
if (closeErr) return handleFatalError(closeErr, done);
2024-06-20 18:55:18 +08:00
done();
});
});
2021-06-26 00:11:43 +08:00
}, 40000);
}
it(`${testName} should compile`, done => {
rimraf.sync(outputDirectory);
fs.mkdirSync(outputDirectory, { recursive: true });
2022-02-10 17:41:48 +08:00
infraStructureLog.length = 0;
const deprecationTracker = deprecationTracking.start();
const onCompiled = (err, stats) => {
const deprecations = deprecationTracker();
if (err) return handleFatalError(err, done);
const statOptions = {
preset: "verbose",
colors: false
};
fs.mkdirSync(outputDirectory, { recursive: true });
fs.writeFileSync(
path.join(outputDirectory, "stats.txt"),
stats.toString(statOptions),
"utf8"
);
const jsonStats = stats.toJson({
errorDetails: true
});
fs.writeFileSync(
path.join(outputDirectory, "stats.json"),
JSON.stringify(jsonStats, null, 2),
"utf8"
);
if (
checkArrayExpectation(
testDirectory,
jsonStats,
"error",
"Error",
2024-11-01 21:55:49 +08:00
options,
done
)
) {
return;
}
if (
checkArrayExpectation(
testDirectory,
jsonStats,
"warning",
"Warning",
2024-11-01 21:55:49 +08:00
options,
done
)
) {
return;
}
const infrastructureLogging = stderr.toString();
if (infrastructureLogging) {
return done(
new Error(
2024-07-31 10:39:30 +08:00
`Errors/Warnings during build:\n${infrastructureLogging}`
)
);
}
if (
checkArrayExpectation(
testDirectory,
{ deprecations },
"deprecation",
"Deprecation",
2024-11-01 21:55:49 +08:00
options,
done
)
) {
return;
}
2022-02-15 02:31:59 +08:00
const infrastructureLogErrors = filterInfraStructureErrors(
infraStructureLog,
{
run: 3,
options
}
);
if (
infrastructureLogErrors.length &&
checkArrayExpectation(
testDirectory,
{ infrastructureLogs: infrastructureLogErrors },
"infrastructureLog",
"infrastructure-log",
"InfrastructureLog",
2024-11-01 21:55:49 +08:00
options,
2022-02-15 02:31:59 +08:00
done
)
) {
return;
2022-02-10 17:41:48 +08:00
}
let filesCount = 0;
if (testConfig.noTests) return process.nextTick(done);
if (testConfig.beforeExecute) testConfig.beforeExecute();
const results = [];
for (let i = 0; i < optionsArr.length; i++) {
const options = optionsArr[i];
const bundlePath = testConfig.findBundle(i, optionsArr[i]);
if (bundlePath) {
filesCount++;
2025-06-20 22:08:04 +08:00
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,
__STATS__: jsonStats,
2025-06-20 22:08:04 +08:00
__STATS_I__: i
});
if (testConfig.moduleScope) {
2025-06-20 22:08:04 +08:00
testConfig.moduleScope(runner._moduleScope, options);
}
if (Array.isArray(bundlePath)) {
for (const bundlePathItem of bundlePath) {
results.push(
2025-06-20 22:08:04 +08:00
runner.require(outputDirectory, `./${bundlePathItem}`)
);
}
} else {
2025-06-20 22:08:04 +08:00
results.push(runner.require(outputDirectory, bundlePath));
}
}
}
// give a free pass to compilation that generated an error
if (
!jsonStats.errors.length &&
filesCount !== optionsArr.length
) {
return done(
new Error(
"Should have found at least one bundle file per webpack config"
)
);
}
Promise.all(results)
.then(() => {
if (testConfig.afterExecute) {
testConfig.afterExecute(options);
}
2021-07-07 16:00:07 +08:00
for (const key of Object.keys(global)) {
if (key.includes("webpack")) delete global[key];
}
if (getNumberOfTests() < filesCount) {
return done(new Error("No tests exported by test case"));
}
done();
})
.catch(done);
};
if (config.cache) {
try {
const compiler = require("..")(options);
compiler.run(err => {
if (err) return handleFatalError(err, done);
compiler.run((error, stats) => {
compiler.close(err => {
if (err) return handleFatalError(err, done);
onCompiled(error, stats);
});
});
});
2024-07-31 15:37:05 +08:00
} catch (err) {
handleFatalError(err, done);
}
} else {
require("..")(options, onCompiled);
}
2021-07-12 18:07:32 +08:00
}, 30000);
const {
it: _it,
beforeEach: _beforeEach,
afterEach: _afterEach,
setDefaultTimeout,
getNumberOfTests
} = createLazyTestEnv(10000);
});
}
});
}
});
};
2024-07-31 04:54:55 +08:00
// eslint-disable-next-line jest/no-export
module.exports.describeCases = describeCases;