qunitx-cli 0.9.4 → 0.9.8

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.
Files changed (43) hide show
  1. package/bin/qunitx.js +71 -0
  2. package/dist/cli.js +2017 -0
  3. package/package.json +17 -8
  4. package/cli.ts +0 -39
  5. package/deno.json +0 -18
  6. package/deno.lock +0 -646
  7. package/lib/commands/generate.ts +0 -33
  8. package/lib/commands/help.ts +0 -37
  9. package/lib/commands/init.ts +0 -77
  10. package/lib/commands/run/tests-in-browser.ts +0 -221
  11. package/lib/commands/run.ts +0 -279
  12. package/lib/servers/http.ts +0 -321
  13. package/lib/setup/bind-server-to-port.ts +0 -14
  14. package/lib/setup/browser.ts +0 -101
  15. package/lib/setup/config.ts +0 -55
  16. package/lib/setup/default-project-config-values.ts +0 -9
  17. package/lib/setup/file-watcher.ts +0 -134
  18. package/lib/setup/fs-tree.ts +0 -60
  19. package/lib/setup/keyboard-events.ts +0 -38
  20. package/lib/setup/test-file-paths.ts +0 -74
  21. package/lib/setup/web-server.ts +0 -274
  22. package/lib/setup/write-output-static-files.ts +0 -33
  23. package/lib/tap/display-final-result.ts +0 -25
  24. package/lib/tap/display-test-result.ts +0 -109
  25. package/lib/tap/dump-yaml.ts +0 -84
  26. package/lib/types.ts +0 -61
  27. package/lib/utils/chromium-args.ts +0 -18
  28. package/lib/utils/color.ts +0 -66
  29. package/lib/utils/early-chrome.ts +0 -39
  30. package/lib/utils/find-chrome.ts +0 -38
  31. package/lib/utils/find-internal-assets-from-html.ts +0 -18
  32. package/lib/utils/find-project-root.ts +0 -20
  33. package/lib/utils/indent-string.ts +0 -24
  34. package/lib/utils/listen-to-keyboard-key.ts +0 -57
  35. package/lib/utils/parse-cli-flags.ts +0 -95
  36. package/lib/utils/path-exists.ts +0 -21
  37. package/lib/utils/perf-logger.ts +0 -25
  38. package/lib/utils/pre-launch-chrome.ts +0 -45
  39. package/lib/utils/read-boilerplate.ts +0 -15
  40. package/lib/utils/resolve-port-number-for.ts +0 -29
  41. package/lib/utils/run-user-module.ts +0 -28
  42. package/lib/utils/search-in-parent-directories.ts +0 -25
  43. 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
- }
@@ -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
- // ...
@@ -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
- }