qunitx-cli 0.9.1 → 0.9.3
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} +16 -8
- package/deno.lock +20 -14
- package/lib/commands/{generate.js → generate.ts} +5 -5
- package/lib/commands/{help.js → help.ts} +1 -1
- package/lib/commands/{init.js → init.ts} +26 -27
- package/lib/commands/run/{tests-in-browser.js → tests-in-browser.ts} +51 -22
- package/lib/commands/{run.js → run.ts} +100 -39
- package/lib/servers/{http.js → http.ts} +88 -35
- 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} +21 -8
- package/lib/setup/{fs-tree.js → fs-tree.ts} +6 -4
- package/lib/setup/{keyboard-events.js → keyboard-events.ts} +10 -5
- package/lib/setup/{test-file-paths.js → test-file-paths.ts} +10 -4
- package/lib/setup/{web-server.js → web-server.ts} +22 -21
- 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} +25 -11
- 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} +12 -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.ts +45 -0
- 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 +16 -15
- package/lib/setup/bind-server-to-port.js +0 -9
- package/lib/setup/config.js +0 -48
- package/lib/utils/pre-launch-chrome.js +0 -32
- /package/lib/setup/{default-project-config-values.js → default-project-config-values.ts} +0 -0
|
@@ -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
|
|
|
@@ -23,7 +39,7 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
23
39
|
details.fullName.join(' | '),
|
|
24
40
|
`# (${details.runtime.toFixed(0)} ms)`,
|
|
25
41
|
);
|
|
26
|
-
details.assertions.
|
|
42
|
+
details.assertions.forEach((assertion, index) => {
|
|
27
43
|
if (!assertion.passed && assertion.todo === false) {
|
|
28
44
|
COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
|
|
29
45
|
const stack = assertion.stack?.match(/\(.+\)/g);
|
|
@@ -32,7 +48,7 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
32
48
|
console.log(
|
|
33
49
|
indentString(
|
|
34
50
|
dumpYaml({
|
|
35
|
-
name: `Assertion #${index + 1}`,
|
|
51
|
+
name: `Assertion #${index + 1}`,
|
|
36
52
|
actual: assertion.actual
|
|
37
53
|
? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer()))
|
|
38
54
|
: assertion.actual,
|
|
@@ -48,9 +64,7 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
48
64
|
);
|
|
49
65
|
console.log(' ...');
|
|
50
66
|
}
|
|
51
|
-
|
|
52
|
-
return errorCount;
|
|
53
|
-
}, 0);
|
|
67
|
+
});
|
|
54
68
|
} else if (details.status === 'passed') {
|
|
55
69
|
COUNTER.passCount++;
|
|
56
70
|
console.log(
|
|
@@ -61,9 +75,9 @@ export default function TAPDisplayTestResult(COUNTER, details) {
|
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
function getCircularReplacer() {
|
|
65
|
-
const ancestors = [];
|
|
66
|
-
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) {
|
|
67
81
|
if (typeof value !== 'object' || value === null) {
|
|
68
82
|
return value;
|
|
69
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
|
}
|
|
@@ -6,7 +6,7 @@ const LINK_HREF_REGEX = /<link[^>]+\bhref=['"]([^'"]+)['"]/gi;
|
|
|
6
6
|
* Parses an HTML string and returns all internal (non-absolute-URL) `<script src>` and `<link href>` paths.
|
|
7
7
|
* @returns {string[]}
|
|
8
8
|
*/
|
|
9
|
-
export default function findInternalAssetsFromHTML(htmlContent) {
|
|
9
|
+
export default function findInternalAssetsFromHTML(htmlContent: string): string[] {
|
|
10
10
|
const links = [...htmlContent.matchAll(LINK_HREF_REGEX)]
|
|
11
11
|
.map((m) => m[1])
|
|
12
12
|
.filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
-
import searchInParentDirectories from './search-in-parent-directories.
|
|
2
|
+
import searchInParentDirectories from './search-in-parent-directories.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Walks up parent directories from `cwd` to find the nearest `package.json` and returns its directory path.
|
|
6
6
|
* @returns {Promise<string>}
|
|
7
7
|
*/
|
|
8
|
-
export default async function findProjectRoot() {
|
|
8
|
+
export default async function findProjectRoot(): Promise<string> {
|
|
9
9
|
try {
|
|
10
10
|
const absolutePath = await searchInParentDirectories('.', 'package.json');
|
|
11
|
-
if (!absolutePath
|
|
11
|
+
if (!absolutePath!.includes('package.json')) {
|
|
12
12
|
throw new Error('package.json mising');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
return absolutePath
|
|
15
|
+
return absolutePath!.replace('/package.json', '');
|
|
16
16
|
} catch (_error) {
|
|
17
17
|
console.log('couldnt find projects package.json, did you run $ npm init ??');
|
|
18
18
|
process.exit(1);
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
* Prepends `count` repetitions of `indent` (default: one space) to each non-empty line of `string`.
|
|
3
3
|
* @example
|
|
4
4
|
* ```js
|
|
5
|
-
* import indentString from './lib/utils/indent-string.
|
|
5
|
+
* import indentString from './lib/utils/indent-string.ts';
|
|
6
6
|
* console.assert(indentString('hello\nworld', 2) === ' hello\n world');
|
|
7
7
|
* ```
|
|
8
8
|
* @returns {string}
|
|
9
9
|
*/
|
|
10
|
-
export default function indentString(
|
|
10
|
+
export default function indentString(
|
|
11
|
+
string: string,
|
|
12
|
+
count: number = 1,
|
|
13
|
+
options: { indent?: string; includeEmptyLines?: boolean } = {},
|
|
14
|
+
): string {
|
|
11
15
|
const { indent = ' ', includeEmptyLines = false } = options;
|
|
12
16
|
|
|
13
17
|
if (count <= 0) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
|
|
3
3
|
const stdin = process.stdin;
|
|
4
|
-
const targetInputs
|
|
5
|
-
|
|
4
|
+
const targetInputs: Record<string, { caseSensitive: boolean; closure: (input: string) => void }> =
|
|
5
|
+
{};
|
|
6
|
+
const inputs: (string | undefined)[] = [];
|
|
6
7
|
let listenerAdded = false;
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -10,10 +11,11 @@ let listenerAdded = false;
|
|
|
10
11
|
* @returns {void}
|
|
11
12
|
*/
|
|
12
13
|
export default function listenToKeyboardKey(
|
|
13
|
-
inputString,
|
|
14
|
-
closure,
|
|
15
|
-
options = { caseSensitive: false },
|
|
16
|
-
) {
|
|
14
|
+
inputString: string,
|
|
15
|
+
closure: (input: string) => void,
|
|
16
|
+
options: { caseSensitive: boolean } = { caseSensitive: false },
|
|
17
|
+
): void {
|
|
18
|
+
if (!stdin.isTTY) return;
|
|
17
19
|
stdin.setRawMode(true);
|
|
18
20
|
stdin.resume();
|
|
19
21
|
stdin.setEncoding('utf8');
|
|
@@ -43,7 +45,10 @@ export default function listenToKeyboardKey(
|
|
|
43
45
|
targetInputs[inputString.toUpperCase()] = Object.assign(options, { closure });
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
function targetListenerConformsToCase(
|
|
48
|
+
function targetListenerConformsToCase(
|
|
49
|
+
targetListener: { caseSensitive: boolean },
|
|
50
|
+
inputString: string,
|
|
51
|
+
): boolean {
|
|
47
52
|
if (targetListener.caseSensitive) {
|
|
48
53
|
return inputString === inputString.toUpperCase();
|
|
49
54
|
}
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
// { inputs: [], debug: true, watch: true, failFast: true, htmlPaths: [], output }
|
|
2
|
+
interface ParsedFlags {
|
|
3
|
+
inputs: string[];
|
|
4
|
+
debug?: boolean;
|
|
5
|
+
watch?: boolean;
|
|
6
|
+
failFast?: boolean;
|
|
7
|
+
timeout?: number;
|
|
8
|
+
output?: string;
|
|
9
|
+
htmlPaths?: string[];
|
|
10
|
+
port?: number;
|
|
11
|
+
extensions?: string[];
|
|
12
|
+
browser?: 'chromium' | 'firefox' | 'webkit';
|
|
13
|
+
before?: string | false;
|
|
14
|
+
after?: string | false;
|
|
15
|
+
}
|
|
16
|
+
|
|
2
17
|
/**
|
|
3
18
|
* Parses `process.argv` into a qunitx flag object (`inputs`, `debug`, `watch`, `failFast`, `timeout`, `output`, `port`, `before`, `after`).
|
|
4
19
|
* @returns {object}
|
|
5
20
|
*/
|
|
6
|
-
export default function parseCliFlags(projectRoot) {
|
|
21
|
+
export default function parseCliFlags(projectRoot: string): ParsedFlags {
|
|
7
22
|
const providedFlags = process.argv.slice(2).reduce(
|
|
8
23
|
(result, arg) => {
|
|
9
24
|
if (arg.startsWith('--debug')) {
|
|
@@ -41,7 +56,7 @@ export default function parseCliFlags(projectRoot) {
|
|
|
41
56
|
);
|
|
42
57
|
process.exit(1);
|
|
43
58
|
}
|
|
44
|
-
return Object.assign(result, { browser: value });
|
|
59
|
+
return Object.assign(result, { browser: value as 'chromium' | 'firefox' | 'webkit' });
|
|
45
60
|
} else if (arg.startsWith('--before')) {
|
|
46
61
|
return Object.assign(result, { before: parseModule(arg.split('=')[1]) });
|
|
47
62
|
} else if (arg.startsWith('--after')) {
|
|
@@ -55,15 +70,13 @@ export default function parseCliFlags(projectRoot) {
|
|
|
55
70
|
|
|
56
71
|
return result;
|
|
57
72
|
},
|
|
58
|
-
{ inputs: new Set([]) },
|
|
73
|
+
{ inputs: new Set<string>([]) } as ParsedFlags & { inputs: Set<string> },
|
|
59
74
|
);
|
|
60
75
|
|
|
61
|
-
providedFlags
|
|
62
|
-
|
|
63
|
-
return providedFlags;
|
|
76
|
+
return { ...providedFlags, inputs: Array.from(providedFlags.inputs) };
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
function parseBoolean(result, defaultValue = true) {
|
|
79
|
+
function parseBoolean(result: string, defaultValue = true): boolean {
|
|
67
80
|
if (result === 'true') {
|
|
68
81
|
return true;
|
|
69
82
|
} else if (result === 'false') {
|
|
@@ -73,7 +86,7 @@ function parseBoolean(result, defaultValue = true) {
|
|
|
73
86
|
return defaultValue;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
function parseModule(value) {
|
|
89
|
+
function parseModule(value: string): string | false {
|
|
77
90
|
if (['false', "'false'", '"false"', ''].includes(value)) {
|
|
78
91
|
return false;
|
|
79
92
|
}
|
|
@@ -4,13 +4,13 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
* Returns `true` if the given filesystem path is accessible, `false` otherwise.
|
|
5
5
|
* @example
|
|
6
6
|
* ```js
|
|
7
|
-
* import pathExists from './lib/utils/path-exists.
|
|
7
|
+
* import pathExists from './lib/utils/path-exists.ts';
|
|
8
8
|
* console.assert(await pathExists('/tmp') === true);
|
|
9
9
|
* console.assert(await pathExists('/tmp/nonexistent-qunitx-file') === false);
|
|
10
10
|
* ```
|
|
11
11
|
* @returns {Promise<boolean>}
|
|
12
12
|
*/
|
|
13
|
-
export default async function pathExists(path) {
|
|
13
|
+
export default async function pathExists(path: string): Promise<boolean> {
|
|
14
14
|
try {
|
|
15
15
|
await fs.access(path);
|
|
16
16
|
|
|
@@ -17,7 +17,7 @@ const processStart = isPerfTracing ? Date.now() : 0;
|
|
|
17
17
|
* @param {string} label
|
|
18
18
|
* @param {...*} details
|
|
19
19
|
*/
|
|
20
|
-
export function perfLog(label, ...details) {
|
|
20
|
+
export function perfLog(label: string, ...details: unknown[]): void {
|
|
21
21
|
if (!isPerfTracing) return;
|
|
22
22
|
const elapsed = Date.now() - processStart;
|
|
23
23
|
const suffix = details.length ? ' ' + details.join(' ') : '';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { EarlyChrome } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
const CDP_URL_REGEX = /DevTools listening on (ws:\/\/[^\s]+)/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Spawns a headless Chrome process with remote-debugging-port=0 and resolves once the
|
|
8
|
+
* CDP WebSocket endpoint is printed to stderr. Returns null if Chrome is unavailable or
|
|
9
|
+
* fails to start, so callers can fall back to playwright's normal launch.
|
|
10
|
+
* @returns {Promise<{proc: ChildProcess, cdpEndpoint: string} | null>}
|
|
11
|
+
*/
|
|
12
|
+
export default function preLaunchChrome(
|
|
13
|
+
chromePath: string | null | undefined,
|
|
14
|
+
args: string[],
|
|
15
|
+
): Promise<EarlyChrome | null> {
|
|
16
|
+
if (!chromePath) return Promise.resolve(null);
|
|
17
|
+
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const proc = spawn(chromePath, ['--remote-debugging-port=0', '--headless=new', ...args], {
|
|
20
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
let buffer = '';
|
|
24
|
+
proc.stderr.on('data', (chunk) => {
|
|
25
|
+
buffer += chunk.toString();
|
|
26
|
+
const match = buffer.match(CDP_URL_REGEX);
|
|
27
|
+
if (match) {
|
|
28
|
+
// Unref so Chrome's process + stderr pipe don't keep the Node.js event loop alive
|
|
29
|
+
// after all test work is done. Chrome is still killed via process.on('exit').
|
|
30
|
+
proc.unref();
|
|
31
|
+
proc.stderr.unref();
|
|
32
|
+
resolve({ proc, cdpEndpoint: match[1] });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Resolve null on any startup failure so launchBrowser falls back to chromium.launch().
|
|
37
|
+
// The close handler resolves unconditionally: if Chrome exits before printing its CDP URL
|
|
38
|
+
// (code=0 for a clean exit, code=null for a signal-killed process such as OOM on CI),
|
|
39
|
+
// the original condition `code !== null && code !== 0` would leave the promise pending
|
|
40
|
+
// forever, causing launchBrowser to hang and the event loop to drain silently (exit 0).
|
|
41
|
+
// If Chrome already printed its URL and the promise is resolved, this is a no-op.
|
|
42
|
+
proc.on('error', () => resolve(null));
|
|
43
|
+
proc.on('close', () => resolve(null));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -8,7 +8,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
8
8
|
* Reads a boilerplate file by relative path, using the SEA asset store when running as a Node.js binary.
|
|
9
9
|
* @returns {Promise<string>}
|
|
10
10
|
*/
|
|
11
|
-
export default async function readBoilerplate(relativePath) {
|
|
11
|
+
export default async function readBoilerplate(relativePath: string): Promise<string> {
|
|
12
12
|
const sea = await import('node:sea').catch(() => null);
|
|
13
13
|
if (sea?.isSea()) return sea.getAsset(relativePath, 'utf8');
|
|
14
14
|
return (await fs.readFile(join(__dirname, '../../templates', relativePath))).toString();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Returns `portNumber` if it is free, otherwise recursively tries `portNumber + 1` until a free port is found.
|
|
3
3
|
* @returns {Promise<number>}
|
|
4
4
|
*/
|
|
5
|
-
export default async function resolvePortNumberFor(portNumber) {
|
|
5
|
+
export default async function resolvePortNumberFor(portNumber: number): Promise<number> {
|
|
6
6
|
if (await portIsAvailable(portNumber)) {
|
|
7
7
|
return portNumber;
|
|
8
8
|
}
|
|
@@ -10,7 +10,7 @@ export default async function resolvePortNumberFor(portNumber) {
|
|
|
10
10
|
return await resolvePortNumberFor(portNumber + 1);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
async function portIsAvailable(portNumber) {
|
|
13
|
+
async function portIsAvailable(portNumber: number): Promise<boolean> {
|
|
14
14
|
const net = await import('net');
|
|
15
15
|
return new Promise((resolve) => {
|
|
16
16
|
const server = net.createServer();
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { red } from './color.
|
|
1
|
+
import { red } from './color.ts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Dynamically imports `modulePath` and calls its default export with `params`; exits with code 1 on error.
|
|
5
5
|
* @returns {Promise<void>}
|
|
6
6
|
*/
|
|
7
|
-
export default async function runUserModule(
|
|
7
|
+
export default async function runUserModule(
|
|
8
|
+
modulePath: string,
|
|
9
|
+
params: unknown,
|
|
10
|
+
scriptPosition: string,
|
|
11
|
+
): Promise<void> {
|
|
8
12
|
try {
|
|
9
13
|
const func = await import(modulePath);
|
|
10
14
|
if (func) {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import pathExists from './path-exists.
|
|
1
|
+
import pathExists from './path-exists.ts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Recursively searches `directory` and its ancestors for a file or folder named `targetEntry`; returns the absolute path or `undefined`.
|
|
5
5
|
* @returns {Promise<string|undefined>}
|
|
6
6
|
*/
|
|
7
|
-
async function searchInParentDirectories(
|
|
7
|
+
async function searchInParentDirectories(
|
|
8
|
+
directory: string,
|
|
9
|
+
targetEntry: string,
|
|
10
|
+
): Promise<string | undefined> {
|
|
8
11
|
const resolvedDirectory = directory === '.' ? process.cwd() : directory;
|
|
9
12
|
|
|
10
13
|
if (await pathExists(`${resolvedDirectory}/${targetEntry}`)) {
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Returns a timer object with a `startTime` Date and a `stop()` method that returns elapsed milliseconds.
|
|
3
3
|
* @example
|
|
4
4
|
* ```js
|
|
5
|
-
* import timeCounter from './lib/utils/time-counter.
|
|
5
|
+
* import timeCounter from './lib/utils/time-counter.ts';
|
|
6
6
|
* const t = timeCounter();
|
|
7
7
|
* const ms = t.stop();
|
|
8
8
|
* console.assert(ms >= 0);
|
|
9
9
|
* ```
|
|
10
10
|
* @returns {{ startTime: Date, stop: () => number }}
|
|
11
11
|
*/
|
|
12
|
-
export default function timeCounter() {
|
|
12
|
+
export default function timeCounter(): { startTime: Date; stop: () => number } {
|
|
13
13
|
const startTime = new Date();
|
|
14
14
|
|
|
15
15
|
return {
|