@rstest/core
Version:
The Rsbuild-based test tool.
619 lines (618 loc) • 29.9 kB
JavaScript
import 'module';
/*#__PURE__*/ import.meta.url;
export const __webpack_ids__ = [
"359"
];
export const __webpack_modules__ = {
"../../node_modules/.pnpm/stacktrace-parser@0.1.11/node_modules/stacktrace-parser/dist/stack-trace-parser.esm.js": function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.d(__webpack_exports__, {
Q: ()=>parse
});
var UNKNOWN_FUNCTION = '<unknown>';
function parse(stackString) {
var lines = stackString.split('\n');
return lines.reduce(function(stack, line) {
var parseResult = parseChrome(line) || parseWinjs(line) || parseGecko(line) || parseNode(line) || parseJSC(line);
if (parseResult) stack.push(parseResult);
return stack;
}, []);
}
var chromeRe = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|rsc|<anonymous>|\/|[a-z]:\\|\\\\).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
var chromeEvalRe = /\((\S*)(?::(\d+))(?::(\d+))\)/;
function parseChrome(line) {
var parts = chromeRe.exec(line);
if (!parts) return null;
var isNative = parts[2] && 0 === parts[2].indexOf('native');
var isEval = parts[2] && 0 === parts[2].indexOf('eval');
var submatch = chromeEvalRe.exec(parts[2]);
if (isEval && null != submatch) {
parts[2] = submatch[1];
parts[3] = submatch[2];
parts[4] = submatch[3];
}
return {
file: isNative ? null : parts[2],
methodName: parts[1] || UNKNOWN_FUNCTION,
arguments: isNative ? [
parts[2]
] : [],
lineNumber: parts[3] ? +parts[3] : null,
column: parts[4] ? +parts[4] : null
};
}
var winjsRe = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|rsc|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i;
function parseWinjs(line) {
var parts = winjsRe.exec(line);
if (!parts) return null;
return {
file: parts[2],
methodName: parts[1] || UNKNOWN_FUNCTION,
arguments: [],
lineNumber: +parts[3],
column: parts[4] ? +parts[4] : null
};
}
var geckoRe = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|rsc|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i;
var geckoEvalRe = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i;
function parseGecko(line) {
var parts = geckoRe.exec(line);
if (!parts) return null;
var isEval = parts[3] && parts[3].indexOf(' > eval') > -1;
var submatch = geckoEvalRe.exec(parts[3]);
if (isEval && null != submatch) {
parts[3] = submatch[1];
parts[4] = submatch[2];
parts[5] = null;
}
return {
file: parts[3],
methodName: parts[1] || UNKNOWN_FUNCTION,
arguments: parts[2] ? parts[2].split(',') : [],
lineNumber: parts[4] ? +parts[4] : null,
column: parts[5] ? +parts[5] : null
};
}
var javaScriptCoreRe = /^\s*(?:([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i;
function parseJSC(line) {
var parts = javaScriptCoreRe.exec(line);
if (!parts) return null;
return {
file: parts[3],
methodName: parts[1] || UNKNOWN_FUNCTION,
arguments: [],
lineNumber: +parts[4],
column: parts[5] ? +parts[5] : null
};
}
var nodeRe = /^\s*at (?:((?:\[object object\])?[^\\/]+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;
function parseNode(line) {
var parts = nodeRe.exec(line);
if (!parts) return null;
return {
file: parts[2],
methodName: parts[1] || UNKNOWN_FUNCTION,
arguments: [],
lineNumber: +parts[3],
column: parts[4] ? +parts[4] : null
};
}
},
"./src/core/index.ts": function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.d(__webpack_exports__, {
createRstest: ()=>createRstest
});
var external_pathe_ = __webpack_require__("pathe");
class SnapshotManager {
summary;
extension = ".snap";
constructor(options){
this.options = options;
this.clear();
}
clear() {
this.summary = emptySummary(this.options);
}
add(result) {
addSnapshotResult(this.summary, result);
}
resolvePath(testPath, context) {
const resolver = this.options.resolveSnapshotPath || (()=>(0, external_pathe_.join)((0, external_pathe_.join)((0, external_pathe_.dirname)(testPath), "__snapshots__"), `${(0, external_pathe_.basename)(testPath)}${this.extension}`));
const path = resolver(testPath, this.extension, context);
return path;
}
resolveRawPath(testPath, rawPath) {
return (0, external_pathe_.isAbsolute)(rawPath) ? rawPath : (0, external_pathe_.resolve)((0, external_pathe_.dirname)(testPath), rawPath);
}
}
function emptySummary(options) {
const summary = {
added: 0,
failure: false,
filesAdded: 0,
filesRemoved: 0,
filesRemovedList: [],
filesUnmatched: 0,
filesUpdated: 0,
matched: 0,
total: 0,
unchecked: 0,
uncheckedKeysByFile: [],
unmatched: 0,
updated: 0,
didUpdate: "all" === options.updateSnapshot
};
return summary;
}
function addSnapshotResult(summary, result) {
if (result.added) summary.filesAdded++;
if (result.fileDeleted) summary.filesRemoved++;
if (result.unmatched) summary.filesUnmatched++;
if (result.updated) summary.filesUpdated++;
summary.added += result.added;
summary.matched += result.matched;
summary.unchecked += result.unchecked;
if (result.uncheckedKeys && result.uncheckedKeys.length > 0) summary.uncheckedKeysByFile.push({
filePath: result.filepath,
keys: result.uncheckedKeys
});
summary.unmatched += result.unmatched;
summary.updated += result.updated;
summary.total += result.added + result.matched + result.unmatched + result.updated;
}
var external_std_env_ = __webpack_require__("std-env");
var src_config = __webpack_require__("./src/config.ts");
var stack_trace_parser_esm = __webpack_require__("../../node_modules/.pnpm/stacktrace-parser@0.1.11/node_modules/stacktrace-parser/dist/stack-trace-parser.esm.js");
var utils = __webpack_require__("./src/utils/index.ts");
var external_node_util_ = __webpack_require__("node:util");
const DEFAULT_RENDER_INTERVAL_MS = 1000;
const ESC = '\x1B[';
const CLEAR_LINE = `${ESC}K`;
const MOVE_CURSOR_ONE_ROW_UP = `${ESC}1A`;
const SYNC_START = `${ESC}?2026h`;
const SYNC_END = `${ESC}?2026l`;
class WindowRenderer {
options;
streams;
buffer = [];
renderInterval = void 0;
renderScheduled = false;
windowHeight = 0;
finished = false;
cleanups = [];
constructor(options){
this.options = {
interval: DEFAULT_RENDER_INTERVAL_MS,
...options
};
this.streams = {
output: options.logger.outputStream.write.bind(options.logger.outputStream),
error: options.logger.errorStream.write.bind(options.logger.errorStream)
};
this.cleanups.push(this.interceptStream(process.stdout, 'output'), this.interceptStream(process.stderr, 'error'));
this.start();
}
start() {
this.finished = false;
this.renderInterval = setInterval(()=>this.schedule(), this.options.interval).unref();
}
stop() {
this.cleanups.splice(0).map((fn)=>fn());
clearInterval(this.renderInterval);
}
finish() {
this.finished = true;
this.flushBuffer();
clearInterval(this.renderInterval);
}
schedule() {
if (!this.renderScheduled) {
this.renderScheduled = true;
this.flushBuffer();
setTimeout(()=>{
this.renderScheduled = false;
}, 100).unref();
}
}
flushBuffer() {
if (0 === this.buffer.length) return this.render();
let current;
for (const next of this.buffer.splice(0)){
if (!current) {
current = next;
continue;
}
if (current.type !== next.type) {
this.render(current.message, current.type);
current = next;
continue;
}
current.message += next.message;
}
if (current) this.render(current?.message, current?.type);
}
render(message, type = 'output') {
if (this.finished) {
this.clearWindow();
return this.write(message || '', type);
}
const windowContent = this.options.getWindow();
const rowCount = getRenderedRowCount(windowContent, this.options.logger.getColumns());
let padding = this.windowHeight - rowCount;
if (padding > 0 && message) padding -= getRenderedRowCount([
message
], this.options.logger.getColumns());
this.write(SYNC_START);
this.clearWindow();
if (message) this.write(message, type);
if (padding > 0) this.write('\n'.repeat(padding));
this.write(windowContent.join('\n'));
this.write(SYNC_END);
this.windowHeight = rowCount + Math.max(0, padding);
}
clearWindow() {
if (0 === this.windowHeight) return;
this.write(CLEAR_LINE);
for(let i = 1; i < this.windowHeight; i++)this.write(`${MOVE_CURSOR_ONE_ROW_UP}${CLEAR_LINE}`);
this.windowHeight = 0;
}
interceptStream(stream, type) {
const original = stream.write.bind(stream);
stream.write = (chunk, _, callback)=>{
if (chunk) if (this.finished) this.write(chunk.toString(), type);
else this.buffer.push({
type,
message: chunk.toString()
});
callback?.();
};
return function restore() {
stream.write = original;
};
}
write(message, type = 'output') {
this.streams[type](message);
}
}
function getRenderedRowCount(rows, columns) {
let count = 0;
for (const row of rows){
const text = (0, external_node_util_.stripVTControlCharacters)(row);
count += Math.max(1, Math.ceil(text.length / columns));
}
return count;
}
class StatusRenderer {
rootPath;
renderer;
runningModules = new Set();
constructor(rootPath){
this.rootPath = rootPath;
this.renderer = new WindowRenderer({
getWindow: ()=>this.getContent(),
logger: {
outputStream: process.stdout,
errorStream: process.stderr,
getColumns: ()=>'columns' in process.stdout ? process.stdout.columns : 80
}
});
}
getContent() {
const summary = [];
for (const module of this.runningModules){
const relativePath = (0, external_pathe_.relative)(this.rootPath, module);
summary.push(`${utils.$_.bgYellow(utils.$_.bold(' RUNS '))} ${(0, utils.aj)(relativePath)}`);
}
summary.push('');
return summary;
}
addRunningModule(testPath) {
this.runningModules.add(testPath);
this.renderer?.schedule();
}
removeRunningModule(testPath) {
this.runningModules.delete(testPath);
this.renderer?.schedule();
}
clear() {
this.runningModules.clear();
this.renderer?.finish();
}
}
const getSummaryStatusString = (tasks, name = 'tests', showTotal = true)=>{
if (0 === tasks.length) return utils.$_.dim(`no ${name}`);
const passed = tasks.filter((result)=>'pass' === result.status);
const failed = tasks.filter((result)=>'fail' === result.status);
const skipped = tasks.filter((result)=>'skip' === result.status);
const todo = tasks.filter((result)=>'todo' === result.status);
const status = [
failed.length ? utils.$_.bold(utils.$_.red(`${failed.length} failed`)) : null,
passed.length ? utils.$_.bold(utils.$_.green(`${passed.length} passed`)) : null,
skipped.length ? utils.$_.yellow(`${skipped.length} skipped`) : null,
todo.length ? utils.$_.gray(`${todo.length} todo`) : null
].filter(Boolean);
return status.join(utils.$_.dim(' | ')) + (showTotal && status.length > 1 ? utils.$_.gray(` (${tasks.length})`) : '');
};
const printSnapshotSummaryLog = (snapshots, rootDir)=>{
const summary = [];
if (snapshots.added) summary.push(utils.$_.bold(utils.$_.green(`${snapshots.added} written`)));
if (snapshots.unmatched) summary.push(utils.$_.bold(utils.$_.red(`${snapshots.unmatched} failed`)));
if (snapshots.updated) summary.push(utils.$_.bold(utils.$_.green(`${snapshots.updated} updated `)));
if (snapshots.filesRemoved) if (snapshots.didUpdate) summary.push(utils.$_.bold(utils.$_.green(`${snapshots.filesRemoved} files removed `)));
else summary.push(utils.$_.bold(utils.$_.yellow(`${snapshots.filesRemoved} files obsolete `)));
const POINTER = "\u279C";
if (snapshots.filesRemovedList?.length) {
const [head, ...tail] = snapshots.filesRemovedList;
summary.push(`${utils.$_.gray(POINTER)} ${(0, utils.Ps)(rootDir, head)}`);
for (const key of tail)summary.push(` ${(0, utils.Ps)(rootDir, key)}`);
}
if (snapshots.unchecked) {
if (snapshots.didUpdate) summary.push(utils.$_.bold(utils.$_.green(`${snapshots.unchecked} removed`)));
else summary.push(utils.$_.bold(utils.$_.yellow(`${snapshots.unchecked} obsolete`)));
for (const uncheckedFile of snapshots.uncheckedKeysByFile){
summary.push(`${utils.$_.gray(POINTER)} ${(0, utils.Ps)(rootDir, uncheckedFile.filePath)}`);
for (const key of uncheckedFile.keys)summary.push(` ${key}`);
}
}
for (const [index, snapshot] of summary.entries()){
const title = 0 === index ? 'Snapshots' : '';
utils.kg.log(`${utils.$_.gray(title.padStart(12))} ${snapshot}`);
}
};
const printSummaryLog = ({ results, testResults, snapshotSummary, duration, rootPath })=>{
utils.kg.log('');
printSnapshotSummaryLog(snapshotSummary, rootPath);
utils.kg.log(`${utils.$_.gray('Test Files'.padStart(11))} ${getSummaryStatusString(results)}`);
utils.kg.log(`${utils.$_.gray('Tests'.padStart(11))} ${getSummaryStatusString(testResults)}`);
utils.kg.log(`${utils.$_.gray('Duration'.padStart(11))} ${(0, utils.AS)(duration.totalTime)} ${utils.$_.gray(`(build ${(0, utils.AS)(duration.buildTime)}, tests ${(0, utils.AS)(duration.testTime)})`)}`);
utils.kg.log('');
};
const printSummaryErrorLogs = async ({ testResults, results, rootPath, getSourcemap })=>{
const failedTests = [
...results.filter((i)=>'fail' === i.status && i.errors?.length),
...testResults.filter((i)=>'fail' === i.status)
];
if (0 === failedTests.length) return;
utils.kg.log('');
utils.kg.log(utils.$_.bold('Summary of all failing tests:'));
utils.kg.log('');
for (const test of failedTests){
const relativePath = external_pathe_["default"].relative(rootPath, test.testPath);
const nameStr = (0, utils.Yz)(test);
utils.kg.log(`${utils.$_.bgRed(' FAIL ')} ${(0, utils.aj)(relativePath)} ${nameStr.length ? `${utils.$_.dim(utils.Qd)} ${nameStr}` : ''}`);
if (test.errors) {
const { printError } = await Promise.all([
__webpack_require__.e("723"),
__webpack_require__.e("355")
]).then(__webpack_require__.bind(__webpack_require__, "./src/utils/error.ts"));
for (const error of test.errors)await printError(error, getSourcemap, rootPath);
}
}
};
const statusStr = {
fail: "\u2717",
pass: "\u2713",
todo: '-',
skip: '-'
};
const statusColorfulStr = {
fail: utils.$_.red(statusStr.fail),
pass: utils.$_.green(statusStr.pass),
todo: utils.$_.gray(statusStr.todo),
skip: utils.$_.gray(statusStr.skip)
};
const logCase = (result, slowTestThreshold)=>{
const isSlowCase = (result.duration || 0) > slowTestThreshold;
const icon = isSlowCase && 'pass' === result.status ? utils.$_.yellow(statusStr[result.status]) : statusColorfulStr[result.status];
const nameStr = (0, utils.Yz)(result);
const duration = void 0 !== result.duration ? ` (${(0, utils.AS)(result.duration)})` : '';
const retry = result.retryCount ? utils.$_.yellow(` (retry x${result.retryCount})`) : '';
utils.kg.log(` ${icon} ${nameStr}${utils.$_.gray(duration)}${retry}`);
if (result.errors) for (const error of result.errors)console.error(utils.$_.red(` ${error.message}`));
};
const logFileTitle = (test, relativePath, slowTestThreshold, alwaysShowTime = false)=>{
let title = ` ${utils.$_.bold(statusColorfulStr[test.status])} ${(0, utils.aj)(relativePath)}`;
const formatDuration = (duration)=>utils.$_[duration > slowTestThreshold ? 'yellow' : 'green']((0, utils.AS)(duration));
title += ` ${utils.$_.gray(`(${test.results.length})`)}`;
const isTooSlow = test.duration && test.duration > slowTestThreshold;
if (alwaysShowTime || isTooSlow) title += ` ${formatDuration(test.duration)}`;
utils.kg.log(title);
};
class DefaultReporter {
rootPath;
config;
options = {};
statusRenderer;
constructor({ rootPath, options, config }){
this.rootPath = rootPath;
this.config = config;
this.options = options;
if (!external_std_env_.isCI) this.statusRenderer = new StatusRenderer(rootPath);
}
onTestFileStart(test) {
this.statusRenderer?.addRunningModule(test.testPath);
}
onTestFileResult(test) {
this.statusRenderer?.removeRunningModule(test.testPath);
const relativePath = (0, external_pathe_.relative)(this.rootPath, test.testPath);
const { slowTestThreshold } = this.config;
logFileTitle(test, relativePath, slowTestThreshold);
const isTooSlow = test.duration && test.duration > slowTestThreshold;
const hasRetryCase = test.results.some((result)=>(result.retryCount || 0) > 0);
if ('fail' !== test.status && !isTooSlow && !hasRetryCase) return;
const showAllCases = isTooSlow && !test.results.some((result)=>(result.duration || 0) > slowTestThreshold);
for (const result of test.results){
const isSlowCase = (result.duration || 0) > slowTestThreshold;
const retried = (result.retryCount || 0) > 0;
if (showAllCases || 'fail' === result.status || isSlowCase || retried) logCase(result, slowTestThreshold);
}
}
onTestCaseResult(_result) {}
onUserConsoleLog(log) {
const shouldLog = this.config.onConsoleLog?.(log.content) ?? true;
if (!shouldLog) return;
const titles = [
log.name
];
const testPath = (0, external_pathe_.relative)(this.rootPath, log.testPath);
if (log.trace) {
const [frame] = (0, stack_trace_parser_esm.Q)(log.trace);
const filePath = (0, external_pathe_.relative)(this.rootPath, frame.file || '');
if (filePath !== testPath) titles.push((0, utils.aj)(testPath));
titles.push((0, utils.aj)(filePath) + utils.$_.gray(`:${frame.lineNumber}:${frame.column}`));
} else titles.push((0, utils.aj)(testPath));
utils.kg.log(titles.join(utils.$_.gray(' | ')));
utils.kg.log(log.content);
utils.kg.log('');
}
async onExit() {
this.statusRenderer?.clear();
}
async onTestRunEnd({ results, testResults, duration, getSourcemap, snapshotSummary }) {
this.statusRenderer?.clear();
if (false === this.options.summary) return;
await printSummaryErrorLogs({
testResults,
results,
rootPath: this.rootPath,
getSourcemap
});
printSummaryLog({
results,
testResults,
duration,
rootPath: this.rootPath,
snapshotSummary
});
}
}
class GithubActionsReporter {
onWritePath;
rootPath;
constructor({ options, rootPath }){
this.onWritePath = options.onWritePath;
this.rootPath = rootPath;
}
async onTestRunEnd({ results, testResults, getSourcemap }) {
const failedTests = [
...results.filter((i)=>'fail' === i.status && i.errors?.length),
...testResults.filter((i)=>'fail' === i.status)
];
if (0 === failedTests.length) return;
const { parseErrorStacktrace } = await Promise.all([
__webpack_require__.e("723"),
__webpack_require__.e("355")
]).then(__webpack_require__.bind(__webpack_require__, "./src/utils/error.ts"));
for (const test of failedTests){
const { testPath } = test;
const nameStr = (0, utils.Yz)(test);
const shortPath = (0, external_pathe_.relative)(this.rootPath, testPath);
const title = `${shortPath} ${utils.Qd} ${nameStr}`;
for (const error of test.errors || []){
let file = testPath;
let line = 1;
let column = 1;
const message = `${error.message}${error.diff ? `\n${error.diff}` : ''}`;
const type = 'error';
if (error.stack) {
const stackFrames = await parseErrorStacktrace({
stack: error.stack,
fullStack: error.fullStack,
getSourcemap
});
if (stackFrames[0]) {
file = stackFrames[0].file || test.testPath;
line = stackFrames[0].lineNumber || 1;
column = stackFrames[0].column || 1;
}
}
utils.kg.log(`::${type} file=${this.onWritePath?.(file) || file},line=${line},col=${column},title=${escapeData(title)}::${escapeData(message)}`);
}
}
}
}
function escapeData(s) {
return s.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A').replace(/:/g, '%3A').replace(/,/g, '%2C');
}
class VerboseReporter extends DefaultReporter {
onTestFileResult(test) {
this.statusRenderer?.removeRunningModule(test.testPath);
const relativePath = (0, external_pathe_.relative)(this.rootPath, test.testPath);
const { slowTestThreshold } = this.config;
logFileTitle(test, relativePath, slowTestThreshold, true);
for (const result of test.results)logCase(result, slowTestThreshold);
}
}
var helper = __webpack_require__("./src/utils/helper.ts");
const reportersMap = {
default: DefaultReporter,
verbose: VerboseReporter,
'github-actions': GithubActionsReporter
};
function createReporters(reporters, initOptions = {}) {
const result = (0, helper.XQ)(reporters).map((reporter)=>{
if ('string' == typeof reporter || Array.isArray(reporter)) {
const [name, options = {}] = 'string' == typeof reporter ? [
reporter,
{}
] : reporter;
if (name in reportersMap) {
const Reporter = reportersMap[name];
return new Reporter({
...initOptions,
options
});
}
throw new Error(`Reporter ${reporter} not found. Please install it or use a built-in reporter.`);
}
return reporter;
});
return result;
}
function createContext(options, userConfig) {
const { cwd, command } = options;
const rootPath = userConfig.root ? (0, helper.ZY)(cwd, userConfig.root) : cwd;
const rstestConfig = (0, src_config.hY)(userConfig);
const reporters = 'list' !== command ? createReporters(rstestConfig.reporters, {
rootPath,
config: rstestConfig
}) : [];
const snapshotManager = new SnapshotManager({
updateSnapshot: rstestConfig.update ? 'all' : external_std_env_.isCI ? 'none' : 'new'
});
return {
command,
version: "0.1.3",
rootPath,
reporters,
snapshotManager,
originalConfig: userConfig,
normalizedConfig: rstestConfig
};
}
function createRstest(config, command, fileFilters) {
const context = createContext({
cwd: process.cwd(),
command
}, config);
const runTests = async ()=>{
const { runTests } = await Promise.all([
__webpack_require__.e("854"),
__webpack_require__.e("920")
]).then(__webpack_require__.bind(__webpack_require__, "./src/core/runTests.ts"));
await runTests(context, fileFilters);
};
const listTests = async (options)=>{
const { listTests } = await Promise.all([
__webpack_require__.e("854"),
__webpack_require__.e("285")
]).then(__webpack_require__.bind(__webpack_require__, "./src/core/listTests.ts"));
await listTests(context, fileFilters, options);
};
return {
context,
runTests,
listTests
};
}
}
};