qunitx-cli 0.9.2 → 0.9.4
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/{cli.js → cli.ts} +7 -7
- package/deno.lock +6 -2
- package/lib/commands/{generate.js → generate.ts} +4 -4
- package/lib/commands/{help.js → help.ts} +1 -1
- package/lib/commands/{init.js → init.ts} +15 -7
- package/lib/commands/run/{tests-in-browser.js → tests-in-browser.ts} +28 -14
- package/lib/commands/{run.js → run.ts} +25 -17
- package/lib/servers/{http.js → http.ts} +84 -33
- package/lib/setup/bind-server-to-port.ts +14 -0
- package/lib/setup/{browser.js → browser.ts} +14 -18
- package/lib/setup/config.ts +55 -0
- package/lib/setup/{file-watcher.js → file-watcher.ts} +19 -5
- package/lib/setup/{fs-tree.js → fs-tree.ts} +16 -16
- package/lib/setup/{keyboard-events.js → keyboard-events.ts} +10 -5
- package/lib/setup/{test-file-paths.js → test-file-paths.ts} +20 -32
- package/lib/setup/{web-server.js → web-server.ts} +13 -16
- package/lib/setup/{write-output-static-files.js → write-output-static-files.ts} +6 -2
- package/lib/tap/{display-final-result.js → display-final-result.ts} +6 -3
- package/lib/tap/{display-test-result.js → display-test-result.ts} +22 -6
- package/lib/tap/{dump-yaml.js → dump-yaml.ts} +19 -5
- package/lib/types.ts +61 -0
- package/lib/utils/{chromium-args.js → chromium-args.ts} +1 -1
- package/lib/utils/{color.js → color.ts} +24 -11
- package/lib/utils/{early-chrome.js → early-chrome.ts} +5 -5
- package/lib/utils/{find-chrome.js → find-chrome.ts} +2 -2
- package/lib/utils/{find-internal-assets-from-html.js → find-internal-assets-from-html.ts} +1 -1
- package/lib/utils/{find-project-root.js → find-project-root.ts} +4 -4
- package/lib/utils/{indent-string.js → indent-string.ts} +6 -2
- package/lib/utils/{listen-to-keyboard-key.js → listen-to-keyboard-key.ts} +11 -7
- package/lib/utils/{parse-cli-flags.js → parse-cli-flags.ts} +21 -8
- package/lib/utils/{path-exists.js → path-exists.ts} +2 -2
- package/lib/utils/{perf-logger.js → perf-logger.ts} +1 -1
- package/lib/utils/{pre-launch-chrome.js → pre-launch-chrome.ts} +5 -1
- package/lib/utils/{read-boilerplate.js → read-boilerplate.ts} +1 -1
- package/lib/utils/{resolve-port-number-for.js → resolve-port-number-for.ts} +2 -2
- package/lib/utils/{run-user-module.js → run-user-module.ts} +6 -2
- package/lib/utils/{search-in-parent-directories.js → search-in-parent-directories.ts} +5 -2
- package/lib/utils/{time-counter.js → time-counter.ts} +2 -2
- package/package.json +14 -14
- package/lib/setup/bind-server-to-port.js +0 -9
- package/lib/setup/config.js +0 -48
- /package/lib/setup/{default-project-config-values.js → default-project-config-values.ts} +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import defaultProjectConfigValues from './default-project-config-values.ts';
|
|
3
|
+
import findProjectRoot from '../utils/find-project-root.ts';
|
|
4
|
+
import setupFSTree from './fs-tree.ts';
|
|
5
|
+
import setupTestFilePaths from './test-file-paths.ts';
|
|
6
|
+
import parseCliFlags from '../utils/parse-cli-flags.ts';
|
|
7
|
+
import type { Config } from '../types.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds the merged qunitx config from package.json settings and CLI flags.
|
|
11
|
+
* @returns {Promise<object>}
|
|
12
|
+
*/
|
|
13
|
+
export default async function setupConfig(): Promise<Config> {
|
|
14
|
+
const projectRoot = await findProjectRoot();
|
|
15
|
+
const cliConfigFlags = parseCliFlags(projectRoot);
|
|
16
|
+
const projectPackageJSON = await readConfigFromPackageJSON(projectRoot);
|
|
17
|
+
const inputs = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
|
|
18
|
+
const config = {
|
|
19
|
+
...defaultProjectConfigValues,
|
|
20
|
+
htmlPaths: [] as string[],
|
|
21
|
+
...((projectPackageJSON.qunitx as Partial<Config>) || {}),
|
|
22
|
+
...cliConfigFlags,
|
|
23
|
+
projectRoot,
|
|
24
|
+
inputs,
|
|
25
|
+
testFileLookupPaths: setupTestFilePaths(projectRoot, inputs),
|
|
26
|
+
lastFailedTestFiles: null as string[] | null,
|
|
27
|
+
lastRanTestFiles: null as string[] | null,
|
|
28
|
+
COUNTER: { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 },
|
|
29
|
+
_testRunDone: null as (() => void) | null,
|
|
30
|
+
_resetTestTimeout: null as (() => void) | null,
|
|
31
|
+
};
|
|
32
|
+
config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
|
|
33
|
+
config.fsTree = await setupFSTree(config.testFileLookupPaths, config);
|
|
34
|
+
|
|
35
|
+
return config as Config;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readConfigFromPackageJSON(projectRoot: string) {
|
|
39
|
+
const packageJSON = await fs.readFile(`${projectRoot}/package.json`);
|
|
40
|
+
|
|
41
|
+
return JSON.parse(packageJSON.toString()) as { qunitx?: unknown; [key: string]: unknown };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeHTMLPaths(projectRoot: string, htmlPaths: string[]): string[] {
|
|
45
|
+
return Array.from(new Set(htmlPaths.map((htmlPath) => `${projectRoot}/${htmlPath}`)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readInputsFromPackageJSON(packageJSON: {
|
|
49
|
+
qunitx?: unknown;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}): string[] {
|
|
52
|
+
const qunitx = packageJSON.qunitx as { inputs?: string[] } | undefined;
|
|
53
|
+
|
|
54
|
+
return qunitx && qunitx.inputs ? qunitx.inputs : [];
|
|
55
|
+
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { stat } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { green, magenta, red, yellow } from '../utils/color.
|
|
4
|
+
import { green, magenta, red, yellow } from '../utils/color.ts';
|
|
5
|
+
import type { FSWatcher } from 'node:fs';
|
|
6
|
+
import type { Config, FSTree } from '../types.ts';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Starts `fs.watch` watchers for each lookup path and calls `onEventFunc` on JS/TS file changes, debounced via a flag.
|
|
8
10
|
* Uses `config.fsTree` to distinguish `unlink` (tracked file) from `unlinkDir` (directory) on deletion.
|
|
9
11
|
* @returns {object}
|
|
10
12
|
*/
|
|
11
|
-
export default function setupFileWatchers(
|
|
13
|
+
export default function setupFileWatchers(
|
|
14
|
+
testFileLookupPaths: string[],
|
|
15
|
+
config: Config,
|
|
16
|
+
onEventFunc: (event: string, file: string) => unknown,
|
|
17
|
+
onFinishFunc: ((path: string, event: string) => void) | null | undefined,
|
|
18
|
+
): { fileWatchers: Record<string, FSWatcher>; killFileWatchers: () => Record<string, FSWatcher> } {
|
|
12
19
|
const extensions = config.extensions || ['js', 'ts'];
|
|
13
20
|
const fileWatchers = testFileLookupPaths.reduce((watchers, watchPath) => {
|
|
14
21
|
let ready = false;
|
|
@@ -54,7 +61,14 @@ export default function setupFileWatchers(testFileLookupPaths, config, onEventFu
|
|
|
54
61
|
* `unlinkDir` bypasses the extension filter so deleted directories always clean up fsTree.
|
|
55
62
|
* @returns {void}
|
|
56
63
|
*/
|
|
57
|
-
export function handleWatchEvent(
|
|
64
|
+
export function handleWatchEvent(
|
|
65
|
+
config: Config,
|
|
66
|
+
extensions: string[],
|
|
67
|
+
event: string,
|
|
68
|
+
filePath: string,
|
|
69
|
+
onEventFunc: (event: string, file: string) => unknown,
|
|
70
|
+
onFinishFunc: ((path: string, event: string) => void) | null | undefined,
|
|
71
|
+
): void {
|
|
58
72
|
const isFileEvent = extensions.some((ext) => filePath.endsWith(`.${ext}`));
|
|
59
73
|
|
|
60
74
|
if (!isFileEvent && event !== 'unlinkDir') return;
|
|
@@ -97,7 +111,7 @@ export function handleWatchEvent(config, extensions, event, filePath, onEventFun
|
|
|
97
111
|
* Mutates `fsTree` in place based on a chokidar file-system event.
|
|
98
112
|
* @returns {void}
|
|
99
113
|
*/
|
|
100
|
-
export function mutateFSTree(fsTree, event, path) {
|
|
114
|
+
export function mutateFSTree(fsTree: FSTree, event: string, path: string): void {
|
|
101
115
|
if (event === 'add') {
|
|
102
116
|
fsTree[path] = null;
|
|
103
117
|
} else if (event === 'unlink') {
|
|
@@ -109,7 +123,7 @@ export function mutateFSTree(fsTree, event, path) {
|
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
function getEventColor(event) {
|
|
126
|
+
function getEventColor(event: string): unknown {
|
|
113
127
|
if (event === 'change') {
|
|
114
128
|
return yellow('CHANGED:');
|
|
115
129
|
} else if (event === 'add' || event === 'addDir') {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
1
|
+
import fs, { glob as fsGlob } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import picomatch from 'picomatch';
|
|
3
|
+
import type { FSTree } from '../types.ts';
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
function isGlob(str: string): boolean {
|
|
6
|
+
return /[*?{[]/.test(str);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function readDirRecursive(dir: string, filter: (name: string) => boolean): Promise<string[]> {
|
|
7
10
|
const entries = await fs.readdir(dir, { recursive: true, withFileTypes: true });
|
|
8
11
|
return entries
|
|
9
12
|
.filter((e) => e.isFile() && filter(e.name))
|
|
@@ -14,32 +17,29 @@ async function readDirRecursive(dir, filter) {
|
|
|
14
17
|
* Resolves an array of file paths, directories, or glob patterns into a flat `{ absolutePath: null }` map.
|
|
15
18
|
* @returns {Promise<object>}
|
|
16
19
|
*/
|
|
17
|
-
export default async function buildFSTree(
|
|
20
|
+
export default async function buildFSTree(
|
|
21
|
+
fileAbsolutePaths: string[],
|
|
22
|
+
config: { extensions?: string[] } = {},
|
|
23
|
+
): Promise<FSTree> {
|
|
18
24
|
const targetExtensions = config.extensions || ['js', 'ts'];
|
|
19
25
|
const fsTree = {};
|
|
20
26
|
|
|
21
27
|
await Promise.all(
|
|
22
28
|
fileAbsolutePaths.map(async (fileAbsolutePath) => {
|
|
23
|
-
const glob = picomatch.scan(fileAbsolutePath);
|
|
24
|
-
|
|
25
29
|
try {
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
fileNames.forEach((fileName) => {
|
|
32
|
-
if (picomatch.isMatch(fileName, fileAbsolutePath, { bash: true })) {
|
|
30
|
+
if (isGlob(fileAbsolutePath)) {
|
|
31
|
+
for await (const fileName of fsGlob(fileAbsolutePath)) {
|
|
32
|
+
if (targetExtensions.some((ext) => fileName.endsWith(`.${ext}`))) {
|
|
33
33
|
fsTree[fileName] = null;
|
|
34
34
|
}
|
|
35
|
-
}
|
|
35
|
+
}
|
|
36
36
|
} else {
|
|
37
37
|
const entry = await fs.stat(fileAbsolutePath);
|
|
38
38
|
|
|
39
39
|
if (entry.isFile()) {
|
|
40
40
|
fsTree[fileAbsolutePath] = null;
|
|
41
41
|
} else if (entry.isDirectory()) {
|
|
42
|
-
const fileNames = await readDirRecursive(
|
|
42
|
+
const fileNames = await readDirRecursive(fileAbsolutePath, (name) => {
|
|
43
43
|
return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
|
|
44
44
|
});
|
|
45
45
|
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { blue } from '../utils/color.
|
|
2
|
-
import listenToKeyboardKey from '../utils/listen-to-keyboard-key.
|
|
3
|
-
import runTestsInBrowser from '../commands/run/tests-in-browser.
|
|
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';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Registers watch-mode keyboard shortcuts: `qq` to abort, `qa` to run all, `qf` for last failed, `ql` for last run.
|
|
7
8
|
* @returns {void}
|
|
8
9
|
*/
|
|
9
|
-
export default function setupKeyboardEvents(
|
|
10
|
+
export default function setupKeyboardEvents(
|
|
11
|
+
config: Config,
|
|
12
|
+
cachedContent: CachedContent,
|
|
13
|
+
connections: Connections,
|
|
14
|
+
): void {
|
|
10
15
|
listenToKeyboardKey('qq', () => abortBrowserQUnit(config, connections));
|
|
11
16
|
listenToKeyboardKey('qa', () => {
|
|
12
17
|
abortBrowserQUnit(config, connections);
|
|
@@ -28,6 +33,6 @@ export default function setupKeyboardEvents(config, cachedContent, connections)
|
|
|
28
33
|
});
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
function abortBrowserQUnit(_config, connections) {
|
|
36
|
+
function abortBrowserQUnit(_config: Config, connections: Connections): void {
|
|
32
37
|
connections.server.publish('abort', 'abort');
|
|
33
38
|
}
|
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
+
}
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
14
|
* Deduplicates a list of file, folder, and glob inputs so that more-specific paths covered by broader ones are removed.
|
|
6
15
|
* @returns {string[]}
|
|
7
16
|
*/
|
|
8
|
-
export default function setupTestFilePaths(_projectRoot, inputs) {
|
|
17
|
+
export default function setupTestFilePaths(_projectRoot: string, inputs: string[]): string[] {
|
|
9
18
|
// NOTE: very complex algorithm, order is very important
|
|
10
19
|
const [folders, filesWithGlob, filesWithoutGlob] = inputs.reduce(
|
|
11
20
|
(result, input) => {
|
|
12
|
-
const
|
|
21
|
+
const glob = isGlob(input);
|
|
13
22
|
|
|
14
23
|
if (!pathIsFile(input)) {
|
|
15
|
-
result[0].push({
|
|
16
|
-
input,
|
|
17
|
-
isFile: false,
|
|
18
|
-
isGlob,
|
|
19
|
-
});
|
|
24
|
+
result[0].push({ input, isFile: false, isGlob: glob });
|
|
20
25
|
} else {
|
|
21
|
-
result[
|
|
22
|
-
input,
|
|
23
|
-
isFile: true,
|
|
24
|
-
isGlob,
|
|
25
|
-
});
|
|
26
|
+
result[glob ? 1 : 2].push({ input, isFile: true, isGlob: glob });
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
return result;
|
|
@@ -52,35 +53,22 @@ export default function setupTestFilePaths(_projectRoot, inputs) {
|
|
|
52
53
|
return result.map((metaItem) => metaItem.input);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function pathIsFile(path) {
|
|
56
|
+
function pathIsFile(path: string): boolean {
|
|
56
57
|
const inputs = path.split('/');
|
|
57
58
|
|
|
58
59
|
return inputs[inputs.length - 1].includes('.');
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function pathIsIncludedInPaths(paths, targetPath) {
|
|
62
|
+
function pathIsIncludedInPaths(paths: PathMeta[], targetPath: PathMeta): boolean {
|
|
62
63
|
return paths.some((path) => {
|
|
63
64
|
if (path === targetPath) {
|
|
64
65
|
return false;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return picomatch.isMatch(targetPath.input, globFormat, { bash: true });
|
|
68
|
+
return matchesGlob(targetPath.input, buildGlobFormat(path));
|
|
70
69
|
});
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
function buildGlobFormat(path) {
|
|
74
|
-
|
|
75
|
-
if (!path.isGlob) {
|
|
76
|
-
return `${path.input}/*`;
|
|
77
|
-
} else if (path.input.endsWith('*')) {
|
|
78
|
-
// NOTE: could be problematic in future, investigate if I should check endsWith /*
|
|
79
|
-
return path.input;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return `${path.input}/*`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return path.input;
|
|
72
|
+
function buildGlobFormat(path: PathMeta): string {
|
|
73
|
+
return path.isFile ? path.input : `${path.input}/**`;
|
|
86
74
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.
|
|
4
|
-
import TAPDisplayTestResult from '../tap/display-test-result.
|
|
5
|
-
import pathExists from '../utils/path-exists.
|
|
6
|
-
import HTTPServer, { MIME_TYPES } from '../servers/http.
|
|
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';
|
|
7
8
|
|
|
8
9
|
const fsPromise = fs.promises;
|
|
9
10
|
|
|
@@ -11,15 +12,7 @@ const fsPromise = fs.promises;
|
|
|
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.
|
|
12
13
|
* @returns {object}
|
|
13
14
|
*/
|
|
14
|
-
export default function setupWebServer(
|
|
15
|
-
config = {
|
|
16
|
-
port: 1234,
|
|
17
|
-
debug: false,
|
|
18
|
-
watch: false,
|
|
19
|
-
timeout: 10000,
|
|
20
|
-
},
|
|
21
|
-
cachedContent,
|
|
22
|
-
) {
|
|
15
|
+
export default function setupWebServer(config: Config, cachedContent: CachedContent): HTTPServer {
|
|
23
16
|
const STATIC_FILES_PATH = path.join(config.projectRoot, config.output);
|
|
24
17
|
const server = new HTTPServer();
|
|
25
18
|
|
|
@@ -135,7 +128,7 @@ export default function setupWebServer(
|
|
|
135
128
|
return server;
|
|
136
129
|
}
|
|
137
130
|
|
|
138
|
-
function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
131
|
+
function replaceAssetPaths(html: string, htmlPath: string, projectRoot: string): string {
|
|
139
132
|
const assetPaths = findInternalAssetsFromHTML(html);
|
|
140
133
|
const htmlDirectory = htmlPath.split('/').slice(0, -1).join('/');
|
|
141
134
|
|
|
@@ -146,7 +139,7 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
146
139
|
}, html);
|
|
147
140
|
}
|
|
148
141
|
|
|
149
|
-
function testRuntimeToInject(port, config) {
|
|
142
|
+
function testRuntimeToInject(port: number, config: Config): string {
|
|
150
143
|
return `<script>
|
|
151
144
|
window.testTimeout = 0;
|
|
152
145
|
setInterval(() => {
|
|
@@ -269,7 +262,11 @@ function testRuntimeToInject(port, config) {
|
|
|
269
262
|
</script>`;
|
|
270
263
|
}
|
|
271
264
|
|
|
272
|
-
function escapeAndInjectTestsToHTML(
|
|
265
|
+
function escapeAndInjectTestsToHTML(
|
|
266
|
+
html: string,
|
|
267
|
+
testRuntimeCode: string,
|
|
268
|
+
testContentCode: Buffer | string | null | undefined,
|
|
269
|
+
): string {
|
|
273
270
|
return html.replace(
|
|
274
271
|
'{{content}}',
|
|
275
272
|
testRuntimeCode.replace('{{allTestCode}}', testContentCode).replace('</script>', '<\/script>'), // NOTE: remove this when simple-html-tokenizer PR gets merged
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import type { CachedContent } from '../types.ts';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Copies static HTML files and referenced assets from the project into the configured output directory.
|
|
5
6
|
* @returns {Promise<void>}
|
|
6
7
|
*/
|
|
7
|
-
export default async function writeOutputStaticFiles(
|
|
8
|
+
export default async function writeOutputStaticFiles(
|
|
9
|
+
{ projectRoot, output }: { projectRoot: string; output: string },
|
|
10
|
+
cachedContent: CachedContent,
|
|
11
|
+
): Promise<void> {
|
|
8
12
|
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
9
13
|
const htmlRelativePath = staticHTMLKey.replace(`${projectRoot}/`, '');
|
|
10
14
|
|
|
@@ -24,6 +28,6 @@ export default async function writeOutputStaticFiles({ projectRoot, output }, ca
|
|
|
24
28
|
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
async function ensureFolderExists(assetPath) {
|
|
31
|
+
async function ensureFolderExists(assetPath: string): Promise<void> {
|
|
28
32
|
await fs.mkdir(assetPath.split('/').slice(0, -1).join('/'), { recursive: true });
|
|
29
33
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import type { Counter } from '../types.ts';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Prints the TAP plan line and test-run summary (total, pass, skip, fail, duration).
|
|
3
5
|
* @returns {void}
|
|
4
6
|
*/
|
|
7
|
+
|
|
5
8
|
export default function TAPDisplayFinalResult(
|
|
6
|
-
{ testCount, passCount, skipCount, failCount },
|
|
7
|
-
timeTaken,
|
|
8
|
-
) {
|
|
9
|
+
{ testCount, passCount, skipCount, failCount }: Counter,
|
|
10
|
+
timeTaken: number,
|
|
11
|
+
): void {
|
|
9
12
|
console.log('');
|
|
10
13
|
console.log(`1..${testCount}`);
|
|
11
14
|
console.log(`# tests ${testCount}`);
|
|
@@ -1,5 +1,21 @@
|
|
|
1
|
-
import dumpYaml from './dump-yaml.
|
|
2
|
-
import indentString from '../utils/indent-string.
|
|
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
|
+
}
|
|
3
19
|
|
|
4
20
|
// tape TAP output: ['operator', 'stack', 'at', 'expected', 'actual']
|
|
5
21
|
// ava TAP output: ['message', 'name', 'at', 'assertion', 'values'] // Assertion #5, message
|
|
@@ -7,7 +23,7 @@ import indentString from '../utils/indent-string.js';
|
|
|
7
23
|
* Formats and prints a single QUnit testEnd event as a TAP `ok`/`not ok` line with optional YAML failure block.
|
|
8
24
|
* @returns {void}
|
|
9
25
|
*/
|
|
10
|
-
export default function TAPDisplayTestResult(COUNTER, details) {
|
|
26
|
+
export default function TAPDisplayTestResult(COUNTER: Counter, details: TestDetails): void {
|
|
11
27
|
// NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
12
28
|
COUNTER.testCount++;
|
|
13
29
|
|
|
@@ -59,9 +75,9 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
function getCircularReplacer() {
|
|
63
|
-
const ancestors = [];
|
|
64
|
-
return function (_key, value) {
|
|
78
|
+
function getCircularReplacer(): (_key: string, value: unknown) => unknown {
|
|
79
|
+
const ancestors: object[] = [];
|
|
80
|
+
return function (this: object, _key: string, value: unknown) {
|
|
65
81
|
if (typeof value !== 'object' || value === null) {
|
|
66
82
|
return value;
|
|
67
83
|
}
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
const NEEDS_QUOTING =
|
|
18
18
|
/^$|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
|
|
19
19
|
|
|
20
|
-
function needsQuoting(str) {
|
|
20
|
+
function needsQuoting(str: string): boolean {
|
|
21
21
|
return NEEDS_QUOTING.test(str);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function dumpString(str, indent) {
|
|
24
|
+
function dumpString(str: string, indent: string): string {
|
|
25
25
|
if (str === '') return "''";
|
|
26
26
|
if (str.includes('\n')) {
|
|
27
27
|
// Block scalar |- (strip trailing newline), each line indented by current indent + 2
|
|
@@ -31,7 +31,7 @@ function dumpString(str, indent) {
|
|
|
31
31
|
return str;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function dumpValue(value, indent) {
|
|
34
|
+
function dumpValue(value: unknown, indent: string): string {
|
|
35
35
|
if (value === null || value === undefined) return 'null';
|
|
36
36
|
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
37
37
|
if (typeof value === 'string') return dumpString(value, indent);
|
|
@@ -48,7 +48,7 @@ function dumpValue(value, indent) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// Emits `key: value\n` or `key:\n ...\n` — no trailing space before block scalars.
|
|
51
|
-
function yamlLine(key, value) {
|
|
51
|
+
function yamlLine(key: string, value: unknown): string {
|
|
52
52
|
const v = dumpValue(value, '');
|
|
53
53
|
return v[0] === '\n' ? `${key}:${v}\n` : `${key}: ${v}\n`;
|
|
54
54
|
}
|
|
@@ -58,7 +58,21 @@ function yamlLine(key, value) {
|
|
|
58
58
|
* Uses a template literal (no Object.entries overhead) for the known top-level keys.
|
|
59
59
|
* @returns {string}
|
|
60
60
|
*/
|
|
61
|
-
export default function dumpYaml({
|
|
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 {
|
|
62
76
|
return (
|
|
63
77
|
`name: ${dumpString(name, '')}\n` +
|
|
64
78
|
yamlLine('actual', actual) +
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type HTTPServer from './servers/http.ts';
|
|
2
|
+
import type { Browser, Page } from 'playwright-core';
|
|
3
|
+
import type { ChildProcess } from 'node:child_process';
|
|
4
|
+
import type { Buffer } from 'node:buffer';
|
|
5
|
+
|
|
6
|
+
export interface Counter {
|
|
7
|
+
testCount: number;
|
|
8
|
+
failCount: number;
|
|
9
|
+
skipCount: number;
|
|
10
|
+
passCount: number;
|
|
11
|
+
errorCount: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type FSTree = Record<string, null>;
|
|
15
|
+
|
|
16
|
+
export interface CachedContent {
|
|
17
|
+
allTestCode: Buffer | string | null;
|
|
18
|
+
filteredTestCode?: string;
|
|
19
|
+
assets: Set<string>;
|
|
20
|
+
htmlPathsToRunTests: string[];
|
|
21
|
+
mainHTML: { filePath: string | null; html: string | null };
|
|
22
|
+
staticHTMLs: Record<string, string>;
|
|
23
|
+
dynamicContentHTMLs: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Config {
|
|
27
|
+
output: string;
|
|
28
|
+
timeout: number;
|
|
29
|
+
failFast: boolean;
|
|
30
|
+
port: number;
|
|
31
|
+
extensions: string[];
|
|
32
|
+
browser: 'chromium' | 'firefox' | 'webkit';
|
|
33
|
+
projectRoot: string;
|
|
34
|
+
inputs: string[];
|
|
35
|
+
htmlPaths: string[];
|
|
36
|
+
testFileLookupPaths: string[];
|
|
37
|
+
fsTree: FSTree;
|
|
38
|
+
before?: string | false;
|
|
39
|
+
after?: string | false;
|
|
40
|
+
watch?: boolean;
|
|
41
|
+
debug?: boolean;
|
|
42
|
+
COUNTER: Counter;
|
|
43
|
+
lastFailedTestFiles: string[] | null;
|
|
44
|
+
lastRanTestFiles: string[] | null;
|
|
45
|
+
_testRunDone: (() => void) | null;
|
|
46
|
+
_resetTestTimeout: (() => void) | null;
|
|
47
|
+
_groupMode?: boolean;
|
|
48
|
+
_building?: boolean;
|
|
49
|
+
expressApp?: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Connections {
|
|
53
|
+
server: HTTPServer;
|
|
54
|
+
browser: Browser;
|
|
55
|
+
page: Page;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface EarlyChrome {
|
|
59
|
+
proc: ChildProcess;
|
|
60
|
+
cdpEndpoint: string;
|
|
61
|
+
}
|
|
@@ -5,8 +5,18 @@
|
|
|
5
5
|
* Use `createColors(enabled)` in tests to exercise both enabled and disabled branches directly.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
interface MagentaReturn {
|
|
9
|
+
bold: (t: string) => string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface MagentaFn {
|
|
13
|
+
(text: string): string;
|
|
14
|
+
(): MagentaReturn;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Creates a set of ANSI color helpers with coloring enabled or disabled. */
|
|
18
|
+
export function createColors(enabled: boolean) {
|
|
19
|
+
const c = (open: number, close: number) => (text: string) =>
|
|
10
20
|
enabled ? `\x1b[${open}m${text}\x1b[${close}m` : String(text);
|
|
11
21
|
|
|
12
22
|
const red = c(31, 39);
|
|
@@ -15,10 +25,10 @@ export function createColors(enabled) {
|
|
|
15
25
|
const blue = c(34, 39);
|
|
16
26
|
|
|
17
27
|
/** `magenta(text)` — colored text. `magenta()` — chainable: `.bold(text)`. */
|
|
18
|
-
const magenta = (text) => {
|
|
28
|
+
const magenta = ((text?: string): string | MagentaReturn => {
|
|
19
29
|
if (text !== undefined) return enabled ? `\x1b[35m${text}\x1b[39m` : String(text);
|
|
20
|
-
return { bold: (t) => (enabled ? `\x1b[35m\x1b[1m${t}\x1b[22m\x1b[39m` : String(t)) };
|
|
21
|
-
};
|
|
30
|
+
return { bold: (t: string) => (enabled ? `\x1b[35m\x1b[1m${t}\x1b[22m\x1b[39m` : String(t)) };
|
|
31
|
+
}) as MagentaFn;
|
|
22
32
|
|
|
23
33
|
return { red, green, yellow, blue, magenta };
|
|
24
34
|
}
|
|
@@ -32,22 +42,25 @@ const enabled =
|
|
|
32
42
|
const _c = createColors(enabled);
|
|
33
43
|
|
|
34
44
|
/** ANSI red text. */
|
|
35
|
-
export function red(text) {
|
|
45
|
+
export function red(text: string): string {
|
|
36
46
|
return _c.red(text);
|
|
37
47
|
}
|
|
38
48
|
/** ANSI green text. */
|
|
39
|
-
export function green(text) {
|
|
49
|
+
export function green(text: string): string {
|
|
40
50
|
return _c.green(text);
|
|
41
51
|
}
|
|
42
52
|
/** ANSI yellow text. */
|
|
43
|
-
export function yellow(text) {
|
|
53
|
+
export function yellow(text: string): string {
|
|
44
54
|
return _c.yellow(text);
|
|
45
55
|
}
|
|
46
56
|
/** ANSI blue text. */
|
|
47
|
-
export function blue(text) {
|
|
57
|
+
export function blue(text: string): string {
|
|
48
58
|
return _c.blue(text);
|
|
49
59
|
}
|
|
50
60
|
/** ANSI magenta text. Call without arguments to chain: `magenta().bold(text)`. */
|
|
51
|
-
export function magenta(text)
|
|
52
|
-
|
|
61
|
+
export function magenta(text: string): string;
|
|
62
|
+
/** ANSI magenta text. Call without arguments to chain: `magenta().bold(text)`. */
|
|
63
|
+
export function magenta(): MagentaReturn;
|
|
64
|
+
export function magenta(text?: string): string | MagentaReturn {
|
|
65
|
+
return _c.magenta(text as string);
|
|
53
66
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import findChrome from './find-chrome.
|
|
2
|
-
import preLaunchChrome from './pre-launch-chrome.
|
|
3
|
-
import CHROMIUM_ARGS from './chromium-args.
|
|
4
|
-
import { perfLog } from './perf-logger.
|
|
1
|
+
import findChrome from './find-chrome.ts';
|
|
2
|
+
import preLaunchChrome from './pre-launch-chrome.ts';
|
|
3
|
+
import CHROMIUM_ARGS from './chromium-args.ts';
|
|
4
|
+
import { perfLog } from './perf-logger.ts';
|
|
5
5
|
|
|
6
|
-
// This module is statically imported by cli.
|
|
6
|
+
// This module is statically imported by cli.ts so its module-level code runs
|
|
7
7
|
// at the very start of the process — before the IIFE, before playwright-core loads.
|
|
8
8
|
// For run commands only, Chrome is spawned immediately via CDP so it is ready
|
|
9
9
|
// (or nearly ready) by the time playwright-core finishes loading (~500ms later).
|
|
@@ -11,7 +11,7 @@ const PATH_DIRS = (process.env.PATH || '').split(':').filter(Boolean);
|
|
|
11
11
|
* fs.access/exec-based approaches to be delayed by hundreds of milliseconds.
|
|
12
12
|
* @returns {string|null}
|
|
13
13
|
*/
|
|
14
|
-
function findChromeSync() {
|
|
14
|
+
function findChromeSync(): string | null {
|
|
15
15
|
if (process.env.CHROME_BIN) return process.env.CHROME_BIN;
|
|
16
16
|
|
|
17
17
|
for (const dir of PATH_DIRS) {
|
|
@@ -33,6 +33,6 @@ function findChromeSync() {
|
|
|
33
33
|
* with callers, but the resolution is synchronous.
|
|
34
34
|
* @returns {Promise<string|null>}
|
|
35
35
|
*/
|
|
36
|
-
export default function findChrome() {
|
|
36
|
+
export default function findChrome(): Promise<string | null> {
|
|
37
37
|
return Promise.resolve(findChromeSync());
|
|
38
38
|
}
|