qunitx-cli 0.9.4 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/qunitx.js +71 -0
- package/dist/cli.js +2009 -0
- package/package.json +16 -7
- package/cli.ts +0 -39
- package/deno.json +0 -18
- package/deno.lock +0 -646
- package/lib/commands/generate.ts +0 -33
- package/lib/commands/help.ts +0 -37
- package/lib/commands/init.ts +0 -77
- package/lib/commands/run/tests-in-browser.ts +0 -221
- package/lib/commands/run.ts +0 -279
- package/lib/servers/http.ts +0 -321
- package/lib/setup/bind-server-to-port.ts +0 -14
- package/lib/setup/browser.ts +0 -101
- package/lib/setup/config.ts +0 -55
- package/lib/setup/default-project-config-values.ts +0 -9
- package/lib/setup/file-watcher.ts +0 -134
- package/lib/setup/fs-tree.ts +0 -60
- package/lib/setup/keyboard-events.ts +0 -38
- package/lib/setup/test-file-paths.ts +0 -74
- package/lib/setup/web-server.ts +0 -274
- package/lib/setup/write-output-static-files.ts +0 -33
- package/lib/tap/display-final-result.ts +0 -25
- package/lib/tap/display-test-result.ts +0 -109
- package/lib/tap/dump-yaml.ts +0 -84
- package/lib/types.ts +0 -61
- package/lib/utils/chromium-args.ts +0 -18
- package/lib/utils/color.ts +0 -66
- package/lib/utils/early-chrome.ts +0 -39
- package/lib/utils/find-chrome.ts +0 -38
- package/lib/utils/find-internal-assets-from-html.ts +0 -18
- package/lib/utils/find-project-root.ts +0 -20
- package/lib/utils/indent-string.ts +0 -24
- package/lib/utils/listen-to-keyboard-key.ts +0 -57
- package/lib/utils/parse-cli-flags.ts +0 -95
- package/lib/utils/path-exists.ts +0 -21
- package/lib/utils/perf-logger.ts +0 -25
- package/lib/utils/pre-launch-chrome.ts +0 -45
- package/lib/utils/read-boilerplate.ts +0 -15
- package/lib/utils/resolve-port-number-for.ts +0 -29
- package/lib/utils/run-user-module.ts +0 -28
- package/lib/utils/search-in-parent-directories.ts +0 -25
- package/lib/utils/time-counter.ts +0 -19
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { blue } from '../utils/color.ts';
|
|
2
|
-
import listenToKeyboardKey from '../utils/listen-to-keyboard-key.ts';
|
|
3
|
-
import runTestsInBrowser from '../commands/run/tests-in-browser.ts';
|
|
4
|
-
import type { Config, CachedContent, Connections } from '../types.ts';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Registers watch-mode keyboard shortcuts: `qq` to abort, `qa` to run all, `qf` for last failed, `ql` for last run.
|
|
8
|
-
* @returns {void}
|
|
9
|
-
*/
|
|
10
|
-
export default function setupKeyboardEvents(
|
|
11
|
-
config: Config,
|
|
12
|
-
cachedContent: CachedContent,
|
|
13
|
-
connections: Connections,
|
|
14
|
-
): void {
|
|
15
|
-
listenToKeyboardKey('qq', () => abortBrowserQUnit(config, connections));
|
|
16
|
-
listenToKeyboardKey('qa', () => {
|
|
17
|
-
abortBrowserQUnit(config, connections);
|
|
18
|
-
runTestsInBrowser(config, cachedContent, connections);
|
|
19
|
-
});
|
|
20
|
-
listenToKeyboardKey('qf', () => {
|
|
21
|
-
abortBrowserQUnit(config, connections);
|
|
22
|
-
|
|
23
|
-
if (!config.lastFailedTestFiles) {
|
|
24
|
-
console.log('#', blue(`QUnitX: No tests failed so far, so repeating the last test run`));
|
|
25
|
-
return runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
runTestsInBrowser(config, cachedContent, connections, config.lastFailedTestFiles);
|
|
29
|
-
});
|
|
30
|
-
listenToKeyboardKey('ql', () => {
|
|
31
|
-
abortBrowserQUnit(config, connections);
|
|
32
|
-
runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function abortBrowserQUnit(_config: Config, connections: Connections): void {
|
|
37
|
-
connections.server.publish('abort', 'abort');
|
|
38
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { matchesGlob } from 'node:path';
|
|
2
|
-
|
|
3
|
-
function isGlob(str: string): boolean {
|
|
4
|
-
return /[*?{[]/.test(str);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
interface PathMeta {
|
|
8
|
-
input: string;
|
|
9
|
-
isFile: boolean;
|
|
10
|
-
isGlob: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Deduplicates a list of file, folder, and glob inputs so that more-specific paths covered by broader ones are removed.
|
|
15
|
-
* @returns {string[]}
|
|
16
|
-
*/
|
|
17
|
-
export default function setupTestFilePaths(_projectRoot: string, inputs: string[]): string[] {
|
|
18
|
-
// NOTE: very complex algorithm, order is very important
|
|
19
|
-
const [folders, filesWithGlob, filesWithoutGlob] = inputs.reduce(
|
|
20
|
-
(result, input) => {
|
|
21
|
-
const glob = isGlob(input);
|
|
22
|
-
|
|
23
|
-
if (!pathIsFile(input)) {
|
|
24
|
-
result[0].push({ input, isFile: false, isGlob: glob });
|
|
25
|
-
} else {
|
|
26
|
-
result[glob ? 1 : 2].push({ input, isFile: true, isGlob: glob });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return result;
|
|
30
|
-
},
|
|
31
|
-
[[], [], []],
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const result = folders.reduce((folderResult, folder) => {
|
|
35
|
-
if (!pathIsIncludedInPaths(folders, folder)) {
|
|
36
|
-
folderResult.push(folder);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return folderResult;
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
filesWithGlob.forEach((file) => {
|
|
43
|
-
if (!pathIsIncludedInPaths(result, file) && !pathIsIncludedInPaths(filesWithGlob, file)) {
|
|
44
|
-
result.push(file);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
filesWithoutGlob.forEach((file) => {
|
|
48
|
-
if (!pathIsIncludedInPaths(result, file)) {
|
|
49
|
-
result.push(file);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
return result.map((metaItem) => metaItem.input);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function pathIsFile(path: string): boolean {
|
|
57
|
-
const inputs = path.split('/');
|
|
58
|
-
|
|
59
|
-
return inputs[inputs.length - 1].includes('.');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function pathIsIncludedInPaths(paths: PathMeta[], targetPath: PathMeta): boolean {
|
|
63
|
-
return paths.some((path) => {
|
|
64
|
-
if (path === targetPath) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return matchesGlob(targetPath.input, buildGlobFormat(path));
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function buildGlobFormat(path: PathMeta): string {
|
|
73
|
-
return path.isFile ? path.input : `${path.input}/**`;
|
|
74
|
-
}
|
package/lib/setup/web-server.ts
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.ts';
|
|
4
|
-
import TAPDisplayTestResult from '../tap/display-test-result.ts';
|
|
5
|
-
import pathExists from '../utils/path-exists.ts';
|
|
6
|
-
import HTTPServer, { MIME_TYPES } from '../servers/http.ts';
|
|
7
|
-
import type { Config, CachedContent } from '../types.ts';
|
|
8
|
-
|
|
9
|
-
const fsPromise = fs.promises;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Creates and returns an HTTPServer with routes for the test HTML, filtered test page, and static assets, plus a WebSocket handler that streams TAP events.
|
|
13
|
-
* @returns {object}
|
|
14
|
-
*/
|
|
15
|
-
export default function setupWebServer(config: Config, cachedContent: CachedContent): HTTPServer {
|
|
16
|
-
const STATIC_FILES_PATH = path.join(config.projectRoot, config.output);
|
|
17
|
-
const server = new HTTPServer();
|
|
18
|
-
|
|
19
|
-
server.wss.on('connection', function connection(socket) {
|
|
20
|
-
socket.on('message', function message(data) {
|
|
21
|
-
const { event, details, abort } = JSON.parse(data);
|
|
22
|
-
|
|
23
|
-
if (event === 'connection') {
|
|
24
|
-
if (!config._groupMode) console.log('TAP version 13');
|
|
25
|
-
config._resetTestTimeout?.();
|
|
26
|
-
} else if (event === 'testEnd' && !abort) {
|
|
27
|
-
if (details.status === 'failed') {
|
|
28
|
-
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
config._resetTestTimeout?.();
|
|
32
|
-
TAPDisplayTestResult(config.COUNTER, details);
|
|
33
|
-
} else if (event === 'done') {
|
|
34
|
-
// Signal test completion. TCP ordering guarantees all testEnd messages
|
|
35
|
-
// preceding this on the same connection are already processed by Node.js.
|
|
36
|
-
if (typeof config._testRunDone === 'function') {
|
|
37
|
-
config._testRunDone();
|
|
38
|
-
config._testRunDone = null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
server.get('/', async (_req, res) => {
|
|
45
|
-
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
46
|
-
const htmlContent = escapeAndInjectTestsToHTML(
|
|
47
|
-
replaceAssetPaths(
|
|
48
|
-
cachedContent.mainHTML.html,
|
|
49
|
-
cachedContent.mainHTML.filePath,
|
|
50
|
-
config.projectRoot,
|
|
51
|
-
),
|
|
52
|
-
TEST_RUNTIME_TO_INJECT,
|
|
53
|
-
cachedContent.allTestCode,
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
res.write(htmlContent);
|
|
57
|
-
res.end();
|
|
58
|
-
|
|
59
|
-
return await fsPromise.writeFile(
|
|
60
|
-
`${config.projectRoot}/${config.output}/index.html`,
|
|
61
|
-
htmlContent,
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
server.get('/qunitx.html', async (_req, res) => {
|
|
66
|
-
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
67
|
-
const htmlContent = escapeAndInjectTestsToHTML(
|
|
68
|
-
replaceAssetPaths(
|
|
69
|
-
cachedContent.mainHTML.html,
|
|
70
|
-
cachedContent.mainHTML.filePath,
|
|
71
|
-
config.projectRoot,
|
|
72
|
-
),
|
|
73
|
-
TEST_RUNTIME_TO_INJECT,
|
|
74
|
-
cachedContent.filteredTestCode,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
res.write(htmlContent);
|
|
78
|
-
res.end();
|
|
79
|
-
|
|
80
|
-
return await fsPromise.writeFile(
|
|
81
|
-
`${config.projectRoot}/${config.output}/qunitx.html`,
|
|
82
|
-
htmlContent,
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
server.get('/*', async (req, res) => {
|
|
87
|
-
const possibleDynamicHTML =
|
|
88
|
-
cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
|
|
89
|
-
if (possibleDynamicHTML) {
|
|
90
|
-
const TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
91
|
-
const htmlContent = escapeAndInjectTestsToHTML(
|
|
92
|
-
possibleDynamicHTML,
|
|
93
|
-
TEST_RUNTIME_TO_INJECT,
|
|
94
|
-
cachedContent.allTestCode,
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
res.write(htmlContent);
|
|
98
|
-
res.end();
|
|
99
|
-
|
|
100
|
-
return await fsPromise.writeFile(
|
|
101
|
-
`${config.projectRoot}/${config.output}${req.path}`,
|
|
102
|
-
htmlContent,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const url = req.url;
|
|
107
|
-
const requestStartedAt = new Date();
|
|
108
|
-
const filePath = (
|
|
109
|
-
url.endsWith('/') ? [STATIC_FILES_PATH, url, 'index.html'] : [STATIC_FILES_PATH, url]
|
|
110
|
-
).join('');
|
|
111
|
-
const statusCode = (await pathExists(filePath)) ? 200 : 404;
|
|
112
|
-
|
|
113
|
-
res.writeHead(statusCode, {
|
|
114
|
-
'Content-Type': req.headers.accept?.includes('text/html')
|
|
115
|
-
? MIME_TYPES.html
|
|
116
|
-
: MIME_TYPES[path.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (statusCode === 404) {
|
|
120
|
-
res.end();
|
|
121
|
-
} else {
|
|
122
|
-
fs.createReadStream(filePath).pipe(res);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
console.log(`# [HTTPServer] GET ${url} ${statusCode} - ${new Date() - requestStartedAt}ms`);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
return server;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function replaceAssetPaths(html: string, htmlPath: string, projectRoot: string): string {
|
|
132
|
-
const assetPaths = findInternalAssetsFromHTML(html);
|
|
133
|
-
const htmlDirectory = htmlPath.split('/').slice(0, -1).join('/');
|
|
134
|
-
|
|
135
|
-
return assetPaths.reduce((result, assetPath) => {
|
|
136
|
-
const normalizedFullAbsolutePath = path.normalize(`${htmlDirectory}/${assetPath}`);
|
|
137
|
-
|
|
138
|
-
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, '.'));
|
|
139
|
-
}, html);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function testRuntimeToInject(port: number, config: Config): string {
|
|
143
|
-
return `<script>
|
|
144
|
-
window.testTimeout = 0;
|
|
145
|
-
setInterval(() => {
|
|
146
|
-
window.testTimeout = window.testTimeout + 1000;
|
|
147
|
-
}, 1000);
|
|
148
|
-
|
|
149
|
-
(function() {
|
|
150
|
-
let wsRetryCount = 0;
|
|
151
|
-
const WS_MAX_RETRIES = 50; // 500ms total before giving up
|
|
152
|
-
|
|
153
|
-
function setupWebSocket() {
|
|
154
|
-
try {
|
|
155
|
-
window.socket = new WebSocket('ws://localhost:${port}');
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.log(error);
|
|
158
|
-
retryOrFail();
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
window.socket.addEventListener('open', function() {
|
|
163
|
-
setupQUnit();
|
|
164
|
-
});
|
|
165
|
-
window.socket.addEventListener('error', function() {
|
|
166
|
-
retryOrFail();
|
|
167
|
-
});
|
|
168
|
-
window.socket.addEventListener('message', function(messageEvent) {
|
|
169
|
-
if (!window.IS_PLAYWRIGHT && messageEvent.data === 'refresh') {
|
|
170
|
-
window.location.reload(true);
|
|
171
|
-
} else if (window.IS_PLAYWRIGHT && messageEvent.data === 'abort') {
|
|
172
|
-
window.abortQUnit = true;
|
|
173
|
-
window.QUnit.config.queue.length = 0;
|
|
174
|
-
window.socket.send(JSON.stringify({ event: 'abort' }));
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function retryOrFail() {
|
|
180
|
-
wsRetryCount++;
|
|
181
|
-
if (wsRetryCount > WS_MAX_RETRIES) {
|
|
182
|
-
console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
|
|
183
|
-
window.testTimeout = ${config.timeout};
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
window.setTimeout(setupWebSocket, 10);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
setupWebSocket();
|
|
190
|
-
})();
|
|
191
|
-
|
|
192
|
-
{{allTestCode}}
|
|
193
|
-
|
|
194
|
-
function getCircularReplacer() {
|
|
195
|
-
const ancestors = [];
|
|
196
|
-
return function (key, value) {
|
|
197
|
-
if (typeof value !== "object" || value === null) {
|
|
198
|
-
return value;
|
|
199
|
-
}
|
|
200
|
-
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
201
|
-
ancestors.pop();
|
|
202
|
-
}
|
|
203
|
-
if (ancestors.includes(value)) {
|
|
204
|
-
return "[Circular]";
|
|
205
|
-
}
|
|
206
|
-
ancestors.push(value);
|
|
207
|
-
return value;
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function setupQUnit() {
|
|
212
|
-
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: '' };
|
|
213
|
-
|
|
214
|
-
if (!window.QUnit) {
|
|
215
|
-
console.log('QUnit not found after WebSocket connected');
|
|
216
|
-
window.testTimeout = ${config.timeout};
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
|
|
221
|
-
if (window.IS_PLAYWRIGHT) {
|
|
222
|
-
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
window.QUnit.moduleStart((details) => { // NOTE: might be useful in future for hanged module tracking
|
|
226
|
-
if (window.IS_PLAYWRIGHT) {
|
|
227
|
-
window.socket.send(JSON.stringify({ event: 'moduleStart', details: details }, getCircularReplacer()));
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
window.QUnit.on('testStart', (details) => {
|
|
231
|
-
window.QUNIT_RESULT.totalTests++;
|
|
232
|
-
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
233
|
-
});
|
|
234
|
-
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
235
|
-
window.testTimeout = 0;
|
|
236
|
-
window.QUNIT_RESULT.finishedTests++;
|
|
237
|
-
if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
|
|
238
|
-
window.QUNIT_RESULT.currentTest = null;
|
|
239
|
-
if (window.IS_PLAYWRIGHT) {
|
|
240
|
-
window.socket.send(JSON.stringify({ event: 'testEnd', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
241
|
-
|
|
242
|
-
if (${config.failFast} && details.status === 'failed') {
|
|
243
|
-
window.QUnit.config.queue.length = 0;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
window.QUnit.done((details) => {
|
|
248
|
-
if (window.IS_PLAYWRIGHT) {
|
|
249
|
-
window.socket.send(JSON.stringify({ event: 'done', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
250
|
-
// Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
|
|
251
|
-
// canonical completion signal for Playwright runs. waitForFunction is reserved
|
|
252
|
-
// for true timeouts (test hangs) where testTimeout increments naturally via setInterval.
|
|
253
|
-
// Setting testTimeout after done caused a race: under CI load, waitForFunction could
|
|
254
|
-
// win before Node.js processed the WS done message, dropping all testEnd events.
|
|
255
|
-
} else {
|
|
256
|
-
window.testTimeout = ${config.timeout};
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
window.QUnit.start();
|
|
261
|
-
}
|
|
262
|
-
</script>`;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function escapeAndInjectTestsToHTML(
|
|
266
|
-
html: string,
|
|
267
|
-
testRuntimeCode: string,
|
|
268
|
-
testContentCode: Buffer | string | null | undefined,
|
|
269
|
-
): string {
|
|
270
|
-
return html.replace(
|
|
271
|
-
'{{content}}',
|
|
272
|
-
testRuntimeCode.replace('{{allTestCode}}', testContentCode).replace('</script>', '<\/script>'), // NOTE: remove this when simple-html-tokenizer PR gets merged
|
|
273
|
-
);
|
|
274
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import type { CachedContent } from '../types.ts';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Copies static HTML files and referenced assets from the project into the configured output directory.
|
|
6
|
-
* @returns {Promise<void>}
|
|
7
|
-
*/
|
|
8
|
-
export default async function writeOutputStaticFiles(
|
|
9
|
-
{ projectRoot, output }: { projectRoot: string; output: string },
|
|
10
|
-
cachedContent: CachedContent,
|
|
11
|
-
): Promise<void> {
|
|
12
|
-
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
13
|
-
const htmlRelativePath = staticHTMLKey.replace(`${projectRoot}/`, '');
|
|
14
|
-
|
|
15
|
-
await ensureFolderExists(`${projectRoot}/${output}/${htmlRelativePath}`);
|
|
16
|
-
await fs.writeFile(
|
|
17
|
-
`${projectRoot}/${output}/${htmlRelativePath}`,
|
|
18
|
-
cachedContent.staticHTMLs[staticHTMLKey],
|
|
19
|
-
);
|
|
20
|
-
});
|
|
21
|
-
const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
|
|
22
|
-
const assetRelativePath = assetAbsolutePath.replace(`${projectRoot}/`, '');
|
|
23
|
-
|
|
24
|
-
await ensureFolderExists(`${projectRoot}/${output}/${assetRelativePath}`);
|
|
25
|
-
await fs.copyFile(assetAbsolutePath, `${projectRoot}/${output}/${assetRelativePath}`);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function ensureFolderExists(assetPath: string): Promise<void> {
|
|
32
|
-
await fs.mkdir(assetPath.split('/').slice(0, -1).join('/'), { recursive: true });
|
|
33
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { Counter } from '../types.ts';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Prints the TAP plan line and test-run summary (total, pass, skip, fail, duration).
|
|
5
|
-
* @returns {void}
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export default function TAPDisplayFinalResult(
|
|
9
|
-
{ testCount, passCount, skipCount, failCount }: Counter,
|
|
10
|
-
timeTaken: number,
|
|
11
|
-
): void {
|
|
12
|
-
console.log('');
|
|
13
|
-
console.log(`1..${testCount}`);
|
|
14
|
-
console.log(`# tests ${testCount}`);
|
|
15
|
-
console.log(`# pass ${passCount}`);
|
|
16
|
-
console.log(`# skip ${skipCount}`);
|
|
17
|
-
console.log(`# fail ${failCount}`);
|
|
18
|
-
|
|
19
|
-
// let seconds = timeTaken > 1000 ? Math.floor(timeTaken / 1000) : 0;
|
|
20
|
-
// let milliseconds = timeTaken % 100;
|
|
21
|
-
|
|
22
|
-
console.log(`# duration ${timeTaken}`);
|
|
23
|
-
console.log('');
|
|
24
|
-
}
|
|
25
|
-
// console.log(details.timeTaken); // runtime
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import dumpYaml from './dump-yaml.ts';
|
|
2
|
-
import indentString from '../utils/indent-string.ts';
|
|
3
|
-
import type { Counter } from '../types.ts';
|
|
4
|
-
|
|
5
|
-
interface TestAssertion {
|
|
6
|
-
passed: boolean;
|
|
7
|
-
todo: boolean;
|
|
8
|
-
stack?: string;
|
|
9
|
-
actual?: unknown;
|
|
10
|
-
expected?: unknown;
|
|
11
|
-
message?: string;
|
|
12
|
-
}
|
|
13
|
-
interface TestDetails {
|
|
14
|
-
status: string;
|
|
15
|
-
fullName: string[];
|
|
16
|
-
runtime: number;
|
|
17
|
-
assertions: TestAssertion[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// tape TAP output: ['operator', 'stack', 'at', 'expected', 'actual']
|
|
21
|
-
// ava TAP output: ['message', 'name', 'at', 'assertion', 'values'] // Assertion #5, message
|
|
22
|
-
/**
|
|
23
|
-
* Formats and prints a single QUnit testEnd event as a TAP `ok`/`not ok` line with optional YAML failure block.
|
|
24
|
-
* @returns {void}
|
|
25
|
-
*/
|
|
26
|
-
export default function TAPDisplayTestResult(COUNTER: Counter, details: TestDetails): void {
|
|
27
|
-
// NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
28
|
-
COUNTER.testCount++;
|
|
29
|
-
|
|
30
|
-
if (details.status === 'skipped') {
|
|
31
|
-
COUNTER.skipCount++;
|
|
32
|
-
console.log(`ok ${COUNTER.testCount}`, details.fullName.join(' | '), '# skip');
|
|
33
|
-
} else if (details.status === 'todo') {
|
|
34
|
-
console.log(`not ok ${COUNTER.testCount}`, details.fullName.join(' | '), '# skip');
|
|
35
|
-
} else if (details.status === 'failed') {
|
|
36
|
-
COUNTER.failCount++;
|
|
37
|
-
console.log(
|
|
38
|
-
`not ok ${COUNTER.testCount}`,
|
|
39
|
-
details.fullName.join(' | '),
|
|
40
|
-
`# (${details.runtime.toFixed(0)} ms)`,
|
|
41
|
-
);
|
|
42
|
-
details.assertions.forEach((assertion, index) => {
|
|
43
|
-
if (!assertion.passed && assertion.todo === false) {
|
|
44
|
-
COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
|
|
45
|
-
const stack = assertion.stack?.match(/\(.+\)/g);
|
|
46
|
-
|
|
47
|
-
console.log(' ---');
|
|
48
|
-
console.log(
|
|
49
|
-
indentString(
|
|
50
|
-
dumpYaml({
|
|
51
|
-
name: `Assertion #${index + 1}`,
|
|
52
|
-
actual: assertion.actual
|
|
53
|
-
? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer()))
|
|
54
|
-
: assertion.actual,
|
|
55
|
-
expected: assertion.expected
|
|
56
|
-
? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer()))
|
|
57
|
-
: assertion.expected,
|
|
58
|
-
message: assertion.message || null,
|
|
59
|
-
stack: assertion.stack || null,
|
|
60
|
-
at: stack ? stack[0].replace('(file://', '').replace(')', '') : null,
|
|
61
|
-
}),
|
|
62
|
-
4,
|
|
63
|
-
),
|
|
64
|
-
);
|
|
65
|
-
console.log(' ...');
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
} else if (details.status === 'passed') {
|
|
69
|
-
COUNTER.passCount++;
|
|
70
|
-
console.log(
|
|
71
|
-
`ok ${COUNTER.testCount}`,
|
|
72
|
-
details.fullName.join(' | '),
|
|
73
|
-
`# (${details.runtime.toFixed(0)} ms)`,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function getCircularReplacer(): (_key: string, value: unknown) => unknown {
|
|
79
|
-
const ancestors: object[] = [];
|
|
80
|
-
return function (this: object, _key: string, value: unknown) {
|
|
81
|
-
if (typeof value !== 'object' || value === null) {
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
|
-
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
85
|
-
ancestors.pop();
|
|
86
|
-
}
|
|
87
|
-
if (ancestors.includes(value)) {
|
|
88
|
-
return '[Circular]';
|
|
89
|
-
}
|
|
90
|
-
ancestors.push(value);
|
|
91
|
-
return value;
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// not ok 10 test exited without ending: deepEqual true works
|
|
96
|
-
// ---
|
|
97
|
-
// operator: fail
|
|
98
|
-
// at: process.<anonymous> (/home/izelnakri/ava-test/node_modules/tape/index.js:85:19)
|
|
99
|
-
// stack: |-
|
|
100
|
-
// Error: test exited without ending: deepEqual true works
|
|
101
|
-
// at Test.assert [as _assert] (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:269:54)
|
|
102
|
-
// at Test.bound [as _assert] (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:90:32)
|
|
103
|
-
// at Test.fail (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:363:10)
|
|
104
|
-
// at Test.bound [as fail] (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:90:32)
|
|
105
|
-
// at Test._exit (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:226:14)
|
|
106
|
-
// at Test.bound [as _exit] (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:90:32)
|
|
107
|
-
// at process.<anonymous> (/home/izelnakri/ava-test/node_modules/tape/index.js:85:19)
|
|
108
|
-
// at process.emit (node:events:376:20)
|
|
109
|
-
// ...
|
package/lib/tap/dump-yaml.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal YAML serializer for TAP assertion failure blocks.
|
|
3
|
-
* Handles the fixed schema: { name, actual, expected, message, stack, at }.
|
|
4
|
-
* Values for actual/expected are pre-sanitized via JSON.parse(JSON.stringify(...)).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// Single compiled regex covering all cases where a plain YAML scalar would be misread:
|
|
8
|
-
// - YAML reserved words (null, true, false, ~, yes, no, on, off — case-insensitive)
|
|
9
|
-
// - Starts with a YAML indicator: { [ ! | > ' " % @ `
|
|
10
|
-
// - Block indicators that need a space: - ? : at start of string
|
|
11
|
-
// - Document separator: ---
|
|
12
|
-
// - Looks like a number (integer, float, hex, octal, scientific)
|
|
13
|
-
// - Timestamp-like strings that YAML 1.1 auto-casts to Date
|
|
14
|
-
// - Contains ': ' (key–value) or '#' anywhere (comment)
|
|
15
|
-
// - Empty string
|
|
16
|
-
// Also covers single-letter YAML 1.1 booleans: y/Y → true, n/N → false
|
|
17
|
-
const NEEDS_QUOTING =
|
|
18
|
-
/^$|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
|
|
19
|
-
|
|
20
|
-
function needsQuoting(str: string): boolean {
|
|
21
|
-
return NEEDS_QUOTING.test(str);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function dumpString(str: string, indent: string): string {
|
|
25
|
-
if (str === '') return "''";
|
|
26
|
-
if (str.includes('\n')) {
|
|
27
|
-
// Block scalar |- (strip trailing newline), each line indented by current indent + 2
|
|
28
|
-
return '|-\n' + str.replace(/^/gm, `${indent} `);
|
|
29
|
-
}
|
|
30
|
-
if (needsQuoting(str)) return `'${str.replace(/'/g, "''")}'`;
|
|
31
|
-
return str;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function dumpValue(value: unknown, indent: string): string {
|
|
35
|
-
if (value === null || value === undefined) return 'null';
|
|
36
|
-
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
37
|
-
if (typeof value === 'string') return dumpString(value, indent);
|
|
38
|
-
if (Array.isArray(value)) {
|
|
39
|
-
if (value.length === 0) return '[]';
|
|
40
|
-
const next = `${indent} `;
|
|
41
|
-
return '\n' + value.map((v) => `${next}- ${dumpValue(v, next)}`).join('\n');
|
|
42
|
-
}
|
|
43
|
-
// Plain object
|
|
44
|
-
const entries = Object.entries(value);
|
|
45
|
-
if (entries.length === 0) return '{}';
|
|
46
|
-
const next = `${indent} `;
|
|
47
|
-
return '\n' + entries.map(([k, v]) => `${next}${k}: ${dumpValue(v, next)}`).join('\n');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Emits `key: value\n` or `key:\n ...\n` — no trailing space before block scalars.
|
|
51
|
-
function yamlLine(key: string, value: unknown): string {
|
|
52
|
-
const v = dumpValue(value, '');
|
|
53
|
-
return v[0] === '\n' ? `${key}:${v}\n` : `${key}: ${v}\n`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Serializes the fixed TAP assertion object to a YAML string.
|
|
58
|
-
* Uses a template literal (no Object.entries overhead) for the known top-level keys.
|
|
59
|
-
* @returns {string}
|
|
60
|
-
*/
|
|
61
|
-
export default function dumpYaml({
|
|
62
|
-
name,
|
|
63
|
-
actual,
|
|
64
|
-
expected,
|
|
65
|
-
message,
|
|
66
|
-
stack,
|
|
67
|
-
at,
|
|
68
|
-
}: {
|
|
69
|
-
name: string;
|
|
70
|
-
actual: unknown;
|
|
71
|
-
expected: unknown;
|
|
72
|
-
message: string | null;
|
|
73
|
-
stack: string | null;
|
|
74
|
-
at: string | null;
|
|
75
|
-
}): string {
|
|
76
|
-
return (
|
|
77
|
-
`name: ${dumpString(name, '')}\n` +
|
|
78
|
-
yamlLine('actual', actual) +
|
|
79
|
-
yamlLine('expected', expected) +
|
|
80
|
-
yamlLine('message', message) +
|
|
81
|
-
yamlLine('stack', stack) +
|
|
82
|
-
yamlLine('at', at)
|
|
83
|
-
);
|
|
84
|
-
}
|