qunitx-cli 0.5.5 → 0.5.6
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/deno.json +12 -0
- package/deno.lock +1341 -0
- package/lib/commands/generate.js +7 -2
- package/lib/commands/help.js +2 -1
- package/lib/commands/init.js +3 -2
- package/lib/commands/run/tests-in-browser.js +12 -5
- package/lib/commands/run.js +17 -1
- package/lib/servers/http.js +40 -19
- package/lib/setup/bind-server-to-port.js +4 -0
- package/lib/setup/browser.js +16 -0
- package/lib/setup/config.js +5 -1
- package/lib/setup/default-project-config-values.js +7 -0
- package/lib/setup/file-watcher.js +4 -0
- package/lib/setup/fs-tree.js +6 -0
- package/lib/setup/keyboard-events.js +4 -0
- package/lib/setup/recursive-lookup.d.ts +7 -0
- package/lib/setup/test-file-paths.js +5 -0
- package/lib/setup/web-server.js +7 -7
- package/lib/setup/write-output-static-files.js +4 -0
- package/lib/tap/display-final-result.js +8 -1
- package/lib/tap/display-test-result.js +6 -1
- package/lib/utils/find-chrome.js +4 -0
- package/lib/utils/find-internal-assets-from-html.js +4 -0
- package/lib/utils/find-project-root.js +5 -1
- package/lib/utils/indent-string.js +9 -0
- package/lib/utils/listen-to-keyboard-key.js +4 -0
- package/lib/utils/parse-cli-flags.js +5 -1
- package/lib/utils/path-exists.js +10 -0
- package/lib/utils/read-boilerplate.js +7 -3
- package/lib/utils/resolve-port-number-for.js +4 -0
- package/lib/utils/run-user-module.js +4 -0
- package/lib/utils/search-in-parent-directories.js +4 -0
- package/lib/utils/time-counter.js +12 -1
- package/package.json +6 -2
- package/scripts/lint-docs.js +40 -0
- package/lib/boilerplates/default-project-config-values.js +0 -6
- /package/{lib/boilerplates → templates}/setup/tests.hbs +0 -0
- /package/{lib/boilerplates → templates}/setup/tsconfig.json +0 -0
- /package/{lib/boilerplates → templates}/test.js +0 -0
package/lib/commands/generate.js
CHANGED
|
@@ -4,7 +4,11 @@ import findProjectRoot from '../utils/find-project-root.js';
|
|
|
4
4
|
import pathExists from '../utils/path-exists.js';
|
|
5
5
|
import readBoilerplate from '../utils/read-boilerplate.js';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Generates a new test file from the boilerplate template.
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
export default async function generateTestFiles() {
|
|
8
12
|
const projectRoot = await findProjectRoot();
|
|
9
13
|
const moduleName = process.argv[3]; // TODO: classify this maybe in future
|
|
10
14
|
const path =
|
|
@@ -13,7 +17,8 @@ export default async function () {
|
|
|
13
17
|
: `${projectRoot}/${process.argv[3]}.js`;
|
|
14
18
|
|
|
15
19
|
if (await pathExists(path)) {
|
|
16
|
-
|
|
20
|
+
console.log(`${path} already exists!`);
|
|
21
|
+
return;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
const testJSContent = await readBoilerplate('test.js');
|
package/lib/commands/help.js
CHANGED
|
@@ -4,7 +4,8 @@ import pkg from '../../package.json' with { type: 'json' };
|
|
|
4
4
|
const highlight = (text) => kleur.magenta().bold(text);
|
|
5
5
|
const color = (text) => kleur.blue(text);
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** Prints qunitx-cli usage information to stdout. */
|
|
8
|
+
export default function displayHelpOutput() {
|
|
8
9
|
const config = pkg;
|
|
9
10
|
|
|
10
11
|
console.log(`${highlight('[qunitx v' + config.version + '] Usage:')} qunitx ${color('[targets] --$flags')}
|
package/lib/commands/init.js
CHANGED
|
@@ -2,10 +2,11 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import findProjectRoot from '../utils/find-project-root.js';
|
|
4
4
|
import pathExists from '../utils/path-exists.js';
|
|
5
|
-
import defaultProjectConfigValues from '../
|
|
5
|
+
import defaultProjectConfigValues from '../setup/default-project-config-values.js';
|
|
6
6
|
import readBoilerplate from '../utils/read-boilerplate.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** Bootstraps a new qunitx project: writes the test HTML template, updates package.json, and optionally writes tsconfig.json. */
|
|
9
|
+
export default async function initializeProject() {
|
|
9
10
|
const projectRoot = await findProjectRoot();
|
|
10
11
|
const oldPackageJSON = JSON.parse(await fs.readFile(`${projectRoot}/package.json`));
|
|
11
12
|
const htmlPaths = process.argv.slice(2).reduce(
|
|
@@ -13,7 +13,10 @@ class BundleError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Pre-builds the esbuild bundle for all test files and caches the result in `cachedContent`.
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
17
20
|
export async function buildTestBundle(config, cachedContent) {
|
|
18
21
|
const { projectRoot, output } = config;
|
|
19
22
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
@@ -28,7 +31,7 @@ export async function buildTestBundle(config, cachedContent) {
|
|
|
28
31
|
logLevel: 'error',
|
|
29
32
|
outfile: `${projectRoot}/${output}/tests.js`,
|
|
30
33
|
keepNames: true,
|
|
31
|
-
sourcemap: 'inline',
|
|
34
|
+
sourcemap: config.debug || config.watch ? 'inline' : false,
|
|
32
35
|
}),
|
|
33
36
|
Promise.all(
|
|
34
37
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
@@ -44,6 +47,10 @@ export async function buildTestBundle(config, cachedContent) {
|
|
|
44
47
|
cachedContent.allTestCode = await fs.readFile(`${projectRoot}/${output}/tests.js`);
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Runs the esbuild-bundled tests inside a Puppeteer-controlled browser page and streams TAP output.
|
|
52
|
+
* @returns {Promise<object>}
|
|
53
|
+
*/
|
|
47
54
|
export default async function runTestsInBrowser(
|
|
48
55
|
config,
|
|
49
56
|
cachedContent = {},
|
|
@@ -68,7 +75,7 @@ export default async function runTestsInBrowser(
|
|
|
68
75
|
|
|
69
76
|
if (runHasFilter) {
|
|
70
77
|
const outputPath = `${projectRoot}/${output}/filtered-tests.js`;
|
|
71
|
-
await buildFilteredTests(targetTestFilesToFilter, outputPath);
|
|
78
|
+
await buildFilteredTests(targetTestFilesToFilter, outputPath, config);
|
|
72
79
|
cachedContent.filteredTestCode = (await fs.readFile(outputPath)).toString();
|
|
73
80
|
}
|
|
74
81
|
|
|
@@ -117,7 +124,7 @@ export default async function runTestsInBrowser(
|
|
|
117
124
|
return connections;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
function buildFilteredTests(filteredTests, outputPath) {
|
|
127
|
+
function buildFilteredTests(filteredTests, outputPath, config) {
|
|
121
128
|
return esbuild.build({
|
|
122
129
|
stdin: {
|
|
123
130
|
contents: filteredTests.map((f) => `import "${f}";`).join(''),
|
|
@@ -126,7 +133,7 @@ function buildFilteredTests(filteredTests, outputPath) {
|
|
|
126
133
|
bundle: true,
|
|
127
134
|
logLevel: 'error',
|
|
128
135
|
outfile: outputPath,
|
|
129
|
-
sourcemap: 'inline',
|
|
136
|
+
sourcemap: config.debug || config.watch ? 'inline' : false,
|
|
130
137
|
});
|
|
131
138
|
}
|
|
132
139
|
|
package/lib/commands/run.js
CHANGED
|
@@ -15,7 +15,11 @@ import TAPDisplayFinalResult from '../tap/display-final-result.js';
|
|
|
15
15
|
import findChrome from '../utils/find-chrome.js';
|
|
16
16
|
import readBoilerplate from '../utils/read-boilerplate.js';
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Runs qunitx tests in headless Chrome, either in watch mode or concurrent batch mode.
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
export default async function run(config) {
|
|
19
23
|
const cachedContent = await buildCachedContent(config, config.htmlPaths);
|
|
20
24
|
|
|
21
25
|
if (config.watch) {
|
|
@@ -88,6 +92,18 @@ export default async function (config) {
|
|
|
88
92
|
'--disable-gpu',
|
|
89
93
|
'--remote-debugging-port=0',
|
|
90
94
|
'--window-size=1440,900',
|
|
95
|
+
'--disable-extensions',
|
|
96
|
+
'--disable-sync',
|
|
97
|
+
'--no-first-run',
|
|
98
|
+
'--disable-default-apps',
|
|
99
|
+
'--mute-audio',
|
|
100
|
+
'--disable-background-networking',
|
|
101
|
+
'--disable-background-timer-throttling',
|
|
102
|
+
'--disable-renderer-backgrounding',
|
|
103
|
+
'--disable-dev-shm-usage',
|
|
104
|
+
'--disable-translate',
|
|
105
|
+
'--metrics-recording-only',
|
|
106
|
+
'--disable-hang-monitor',
|
|
91
107
|
],
|
|
92
108
|
executablePath: chromePath,
|
|
93
109
|
headless: true,
|
package/lib/servers/http.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
+
// @deno-types="npm:@types/ws"
|
|
2
3
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
3
4
|
import bindServerToPort from '../setup/bind-server-to-port.js';
|
|
4
5
|
|
|
6
|
+
/** Map of file extensions to their corresponding MIME type strings. */
|
|
5
7
|
export const MIME_TYPES = {
|
|
6
8
|
html: 'text/html; charset=UTF-8',
|
|
7
9
|
js: 'application/javascript',
|
|
@@ -13,7 +15,12 @@ export const MIME_TYPES = {
|
|
|
13
15
|
svg: 'image/svg+xml',
|
|
14
16
|
};
|
|
15
17
|
|
|
18
|
+
/** Minimal HTTP + WebSocket server used to serve test bundles and push reload events. */
|
|
16
19
|
export default class HTTPServer {
|
|
20
|
+
/**
|
|
21
|
+
* Creates and starts a plain `http.createServer` instance on the given port.
|
|
22
|
+
* @returns {Promise<object>}
|
|
23
|
+
*/
|
|
17
24
|
static serve(config = { port: 1234 }, handler) {
|
|
18
25
|
const onListen = config.onListen || ((_server) => {});
|
|
19
26
|
const onError = config.onError || ((_error) => {});
|
|
@@ -60,7 +67,7 @@ export default class HTTPServer {
|
|
|
60
67
|
res.end(JSON.stringify(data));
|
|
61
68
|
};
|
|
62
69
|
|
|
63
|
-
return this
|
|
70
|
+
return this.#handleRequest(req, res);
|
|
64
71
|
});
|
|
65
72
|
this.wss = new WebSocketServer({ server: this._server });
|
|
66
73
|
this.wss.on('error', (error) => {
|
|
@@ -69,14 +76,23 @@ export default class HTTPServer {
|
|
|
69
76
|
});
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Closes the underlying HTTP server.
|
|
81
|
+
* @returns {object}
|
|
82
|
+
*/
|
|
72
83
|
close() {
|
|
73
84
|
return this._server.close();
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
/** Registers a GET route handler. */
|
|
76
88
|
get(path, handler) {
|
|
77
|
-
this
|
|
89
|
+
this.#registerRouteHandler('GET', path, handler);
|
|
78
90
|
}
|
|
79
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Starts listening on the given port (0 = OS-assigned).
|
|
94
|
+
* @returns {Promise<void>}
|
|
95
|
+
*/
|
|
80
96
|
listen(port = 0, callback = () => {}) {
|
|
81
97
|
return new Promise((resolve, reject) => {
|
|
82
98
|
const onError = (err) => {
|
|
@@ -93,6 +109,7 @@ export default class HTTPServer {
|
|
|
93
109
|
});
|
|
94
110
|
}
|
|
95
111
|
|
|
112
|
+
/** Broadcasts a message to all connected WebSocket clients. */
|
|
96
113
|
publish(data) {
|
|
97
114
|
this.wss.clients.forEach((client) => {
|
|
98
115
|
if (client.readyState === WebSocket.OPEN) {
|
|
@@ -101,23 +118,27 @@ export default class HTTPServer {
|
|
|
101
118
|
});
|
|
102
119
|
}
|
|
103
120
|
|
|
121
|
+
/** Registers a POST route handler. */
|
|
104
122
|
post(path, handler) {
|
|
105
|
-
this
|
|
123
|
+
this.#registerRouteHandler('POST', path, handler);
|
|
106
124
|
}
|
|
107
125
|
|
|
126
|
+
/** Registers a DELETE route handler. */
|
|
108
127
|
delete(path, handler) {
|
|
109
|
-
this
|
|
128
|
+
this.#registerRouteHandler('DELETE', path, handler);
|
|
110
129
|
}
|
|
111
130
|
|
|
131
|
+
/** Registers a PUT route handler. */
|
|
112
132
|
put(path, handler) {
|
|
113
|
-
this
|
|
133
|
+
this.#registerRouteHandler('PUT', path, handler);
|
|
114
134
|
}
|
|
115
135
|
|
|
136
|
+
/** Adds a middleware function to the chain. */
|
|
116
137
|
use(middleware) {
|
|
117
138
|
this.middleware.push(middleware);
|
|
118
139
|
}
|
|
119
140
|
|
|
120
|
-
registerRouteHandler(method, path, handler) {
|
|
141
|
+
#registerRouteHandler(method, path, handler) {
|
|
121
142
|
if (!this.routes[method]) {
|
|
122
143
|
this.routes[method] = {};
|
|
123
144
|
}
|
|
@@ -125,22 +146,22 @@ export default class HTTPServer {
|
|
|
125
146
|
this.routes[method][path] = {
|
|
126
147
|
path,
|
|
127
148
|
handler,
|
|
128
|
-
paramNames: this
|
|
149
|
+
paramNames: this.#extractParamNames(path),
|
|
129
150
|
isWildcard: path === '/*',
|
|
130
151
|
};
|
|
131
152
|
}
|
|
132
153
|
|
|
133
|
-
handleRequest(req, res) {
|
|
154
|
+
#handleRequest(req, res) {
|
|
134
155
|
const { method, url } = req;
|
|
135
156
|
const urlObj = new URL(url, 'http://localhost');
|
|
136
157
|
const pathname = urlObj.pathname;
|
|
137
158
|
req.path = pathname;
|
|
138
159
|
req.query = Object.fromEntries(urlObj.searchParams);
|
|
139
|
-
const matchingRoute = this
|
|
160
|
+
const matchingRoute = this.#findRouteHandler(method, pathname);
|
|
140
161
|
|
|
141
162
|
if (matchingRoute) {
|
|
142
|
-
req.params = this
|
|
143
|
-
this
|
|
163
|
+
req.params = this.#extractParams(matchingRoute, pathname);
|
|
164
|
+
this.#runMiddleware(req, res, matchingRoute.handler);
|
|
144
165
|
} else {
|
|
145
166
|
res.statusCode = 404;
|
|
146
167
|
res.setHeader('Content-Type', 'text/plain');
|
|
@@ -148,7 +169,7 @@ export default class HTTPServer {
|
|
|
148
169
|
}
|
|
149
170
|
}
|
|
150
171
|
|
|
151
|
-
runMiddleware(req, res, callback) {
|
|
172
|
+
#runMiddleware(req, res, callback) {
|
|
152
173
|
let index = 0;
|
|
153
174
|
const next = () => {
|
|
154
175
|
if (index >= this.middleware.length) {
|
|
@@ -162,7 +183,7 @@ export default class HTTPServer {
|
|
|
162
183
|
next();
|
|
163
184
|
}
|
|
164
185
|
|
|
165
|
-
findRouteHandler(method, url) {
|
|
186
|
+
#findRouteHandler(method, url) {
|
|
166
187
|
const routes = this.routes[method];
|
|
167
188
|
if (!routes) {
|
|
168
189
|
return null;
|
|
@@ -177,9 +198,9 @@ export default class HTTPServer {
|
|
|
177
198
|
return false;
|
|
178
199
|
}
|
|
179
200
|
|
|
180
|
-
if (isWildcard || this
|
|
201
|
+
if (isWildcard || this.#matchPathSegments(path, url)) {
|
|
181
202
|
if (route.paramNames.length > 0) {
|
|
182
|
-
const regexPattern = this
|
|
203
|
+
const regexPattern = this.#buildRegexPattern(path, route.paramNames);
|
|
183
204
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
184
205
|
const regexMatches = regex.exec(url);
|
|
185
206
|
if (regexMatches) {
|
|
@@ -196,7 +217,7 @@ export default class HTTPServer {
|
|
|
196
217
|
);
|
|
197
218
|
}
|
|
198
219
|
|
|
199
|
-
matchPathSegments(path, url) {
|
|
220
|
+
#matchPathSegments(path, url) {
|
|
200
221
|
const pathSegments = path.split('/');
|
|
201
222
|
const urlSegments = url.split('/');
|
|
202
223
|
|
|
@@ -220,21 +241,21 @@ export default class HTTPServer {
|
|
|
220
241
|
return true;
|
|
221
242
|
}
|
|
222
243
|
|
|
223
|
-
buildRegexPattern(path, _paramNames) {
|
|
244
|
+
#buildRegexPattern(path, _paramNames) {
|
|
224
245
|
let regexPattern = path.replace(/:[^/]+/g, '([^/]+)');
|
|
225
246
|
regexPattern = regexPattern.replace(/\//g, '\\/');
|
|
226
247
|
|
|
227
248
|
return regexPattern;
|
|
228
249
|
}
|
|
229
250
|
|
|
230
|
-
extractParamNames(path) {
|
|
251
|
+
#extractParamNames(path) {
|
|
231
252
|
const paramRegex = /:(\w+)/g;
|
|
232
253
|
const paramMatches = path.match(paramRegex);
|
|
233
254
|
|
|
234
255
|
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
235
256
|
}
|
|
236
257
|
|
|
237
|
-
extractParams(route, _url) {
|
|
258
|
+
#extractParams(route, _url) {
|
|
238
259
|
const { paramNames, paramValues } = route;
|
|
239
260
|
const params = {};
|
|
240
261
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binds an HTTPServer to an OS-assigned port and writes the resolved port back to `config.port`.
|
|
3
|
+
* @returns {Promise<object>}
|
|
4
|
+
*/
|
|
1
5
|
export default async function bindServerToPort(server, config) {
|
|
2
6
|
await server.listen(0);
|
|
3
7
|
config.port = server._server.address().port;
|
package/lib/setup/browser.js
CHANGED
|
@@ -3,6 +3,10 @@ import setupWebServer from './web-server.js';
|
|
|
3
3
|
import bindServerToPort from './bind-server-to-port.js';
|
|
4
4
|
import findChrome from '../utils/find-chrome.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Launches a Puppeteer browser (or reuses an existing one), starts the web server, and returns the page/server/browser connection object.
|
|
8
|
+
* @returns {Promise<{server: object, browser: object, page: object}>}
|
|
9
|
+
*/
|
|
6
10
|
export default async function setupBrowser(
|
|
7
11
|
config = {
|
|
8
12
|
port: 1234,
|
|
@@ -24,6 +28,18 @@ export default async function setupBrowser(
|
|
|
24
28
|
'--disable-gpu',
|
|
25
29
|
'--remote-debugging-port=0',
|
|
26
30
|
'--window-size=1440,900',
|
|
31
|
+
'--disable-extensions',
|
|
32
|
+
'--disable-sync',
|
|
33
|
+
'--no-first-run',
|
|
34
|
+
'--disable-default-apps',
|
|
35
|
+
'--mute-audio',
|
|
36
|
+
'--disable-background-networking',
|
|
37
|
+
'--disable-background-timer-throttling',
|
|
38
|
+
'--disable-renderer-backgrounding',
|
|
39
|
+
'--disable-dev-shm-usage',
|
|
40
|
+
'--disable-translate',
|
|
41
|
+
'--metrics-recording-only',
|
|
42
|
+
'--disable-hang-monitor',
|
|
27
43
|
],
|
|
28
44
|
executablePath: await findChrome(),
|
|
29
45
|
headless: true,
|
package/lib/setup/config.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import defaultProjectConfigValues from '
|
|
2
|
+
import defaultProjectConfigValues from './default-project-config-values.js';
|
|
3
3
|
import findProjectRoot from '../utils/find-project-root.js';
|
|
4
4
|
import setupFSTree from './fs-tree.js';
|
|
5
5
|
import setupTestFilePaths from './test-file-paths.js';
|
|
6
6
|
import parseCliFlags from '../utils/parse-cli-flags.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Builds the merged qunitx config from package.json settings and CLI flags.
|
|
10
|
+
* @returns {Promise<object>}
|
|
11
|
+
*/
|
|
8
12
|
export default async function setupConfig() {
|
|
9
13
|
const projectRoot = await findProjectRoot();
|
|
10
14
|
const cliConfigFlags = parseCliFlags(projectRoot);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import chokidar from 'chokidar';
|
|
2
2
|
import kleur from 'kleur';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Starts chokidar watchers for each lookup path and calls `onEventFunc` on JS/TS file changes, debounced via a global flag.
|
|
6
|
+
* @returns {object}
|
|
7
|
+
*/
|
|
4
8
|
export default function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
5
9
|
const extensions = ['js', 'ts'];
|
|
6
10
|
const fileWatchers = testFileLookupPaths.reduce((watcher, watchPath) => {
|
package/lib/setup/fs-tree.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
// @deno-types="npm:@types/picomatch"
|
|
2
3
|
import picomatch from 'picomatch';
|
|
4
|
+
// @deno-types="./recursive-lookup.d.ts"
|
|
3
5
|
import recursiveLookup from 'recursive-lookup';
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Resolves an array of file paths, directories, or glob patterns into a flat `{ absolutePath: null }` map.
|
|
9
|
+
* @returns {Promise<object>}
|
|
10
|
+
*/
|
|
5
11
|
export default async function buildFSTree(fileAbsolutePaths, _config = {}) {
|
|
6
12
|
const targetExtensions = ['js', 'ts'];
|
|
7
13
|
const fsTree = {};
|
|
@@ -2,6 +2,10 @@ import kleur from 'kleur';
|
|
|
2
2
|
import listenToKeyboardKey from '../utils/listen-to-keyboard-key.js';
|
|
3
3
|
import runTestsInBrowser from '../commands/run/tests-in-browser.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Registers watch-mode keyboard shortcuts: `qq` to abort, `qa` to run all, `qf` for last failed, `ql` for last run.
|
|
7
|
+
* @returns {void}
|
|
8
|
+
*/
|
|
5
9
|
export default function setupKeyboardEvents(config, cachedContent, connections) {
|
|
6
10
|
listenToKeyboardKey('qq', () => abortBrowserQUnit(config, connections));
|
|
7
11
|
listenToKeyboardKey('qa', () => {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
// @deno-types="npm:@types/picomatch"
|
|
1
2
|
import picomatch from 'picomatch';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Deduplicates a list of file, folder, and glob inputs so that more-specific paths covered by broader ones are removed.
|
|
6
|
+
* @returns {string[]}
|
|
7
|
+
*/
|
|
3
8
|
export default function setupTestFilePaths(_projectRoot, inputs) {
|
|
4
9
|
// NOTE: very complex algorithm, order is very important
|
|
5
10
|
const [folders, filesWithGlob, filesWithoutGlob] = inputs.reduce(
|
package/lib/setup/web-server.js
CHANGED
|
@@ -7,6 +7,10 @@ import HTTPServer, { MIME_TYPES } from '../servers/http.js';
|
|
|
7
7
|
|
|
8
8
|
const fsPromise = fs.promises;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* 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
|
+
* @returns {object}
|
|
13
|
+
*/
|
|
10
14
|
export default function setupWebServer(
|
|
11
15
|
config = {
|
|
12
16
|
port: 1234,
|
|
@@ -246,16 +250,12 @@ function testRuntimeToInject(port, config) {
|
|
|
246
250
|
});
|
|
247
251
|
window.QUnit.done((details) => {
|
|
248
252
|
if (window.IS_PUPPETEER) {
|
|
249
|
-
window.
|
|
250
|
-
window.socket.send(JSON.stringify({ event: 'done', details: details, abort: window.abortQUnit }, getCircularReplacer()))
|
|
251
|
-
}, 50);
|
|
253
|
+
window.socket.send(JSON.stringify({ event: 'done', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
252
254
|
}
|
|
253
|
-
window.
|
|
254
|
-
window.testTimeout = ${config.timeout};
|
|
255
|
-
}, 75);
|
|
255
|
+
window.testTimeout = ${config.timeout};
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
-
window.
|
|
258
|
+
window.QUnit.start();
|
|
259
259
|
}
|
|
260
260
|
</script>`;
|
|
261
261
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Copies static HTML files and referenced assets from the project into the configured output directory.
|
|
5
|
+
* @returns {Promise<void>}
|
|
6
|
+
*/
|
|
3
7
|
export default async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
|
|
4
8
|
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
5
9
|
const htmlRelativePath = staticHTMLKey.replace(`${projectRoot}/`, '');
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Prints the TAP plan line and test-run summary (total, pass, skip, fail, duration).
|
|
3
|
+
* @returns {void}
|
|
4
|
+
*/
|
|
5
|
+
export default function TAPDisplayFinalResult(
|
|
6
|
+
{ testCount, passCount, skipCount, failCount },
|
|
7
|
+
timeTaken,
|
|
8
|
+
) {
|
|
2
9
|
console.log('');
|
|
3
10
|
console.log(`1..${testCount}`);
|
|
4
11
|
console.log(`# tests ${testCount}`);
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
// @deno-types="npm:@types/js-yaml"
|
|
1
2
|
import yaml from 'js-yaml';
|
|
2
3
|
import indentString from '../utils/indent-string.js';
|
|
3
4
|
|
|
4
5
|
// tape TAP output: ['operator', 'stack', 'at', 'expected', 'actual']
|
|
5
6
|
// ava TAP output: ['message', 'name', 'at', 'assertion', 'values'] // Assertion #5, message
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Formats and prints a single QUnit testEnd event as a TAP `ok`/`not ok` line with optional YAML failure block.
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
export default function TAPDisplayTestResult(COUNTER, details) {
|
|
7
12
|
// NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
8
13
|
COUNTER.testCount++;
|
|
9
14
|
|
package/lib/utils/find-chrome.js
CHANGED
|
@@ -2,6 +2,10 @@ import { exec } from 'node:child_process';
|
|
|
2
2
|
|
|
3
3
|
const CANDIDATES = ['google-chrome-stable', 'google-chrome', 'chromium', 'chromium-browser'];
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the Chrome/Chromium executable path from `CHROME_BIN` or by probing common binary names.
|
|
7
|
+
* @returns {Promise<string|null>}
|
|
8
|
+
*/
|
|
5
9
|
export default function findChrome() {
|
|
6
10
|
if (process.env.CHROME_BIN) return Promise.resolve(process.env.CHROME_BIN);
|
|
7
11
|
|
|
@@ -2,6 +2,10 @@ import { load } from 'cheerio';
|
|
|
2
2
|
|
|
3
3
|
const ABSOLUTE_URL_REGEX = new RegExp('^(?:[a-z]+:)?//', 'i');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Parses an HTML string and returns all internal (non-absolute-URL) `<script src>` and `<link href>` paths.
|
|
7
|
+
* @returns {string[]}
|
|
8
|
+
*/
|
|
5
9
|
export default function findInternalAssetsFromHTML(htmlContent) {
|
|
6
10
|
const $ = load(htmlContent);
|
|
7
11
|
const internalJSFiles = $('script[src]')
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import searchInParentDirectories from './search-in-parent-directories.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Walks up parent directories from `cwd` to find the nearest `package.json` and returns its directory path.
|
|
6
|
+
* @returns {Promise<string>}
|
|
7
|
+
*/
|
|
8
|
+
export default async function findProjectRoot() {
|
|
5
9
|
try {
|
|
6
10
|
const absolutePath = await searchInParentDirectories('.', 'package.json');
|
|
7
11
|
if (!absolutePath.includes('package.json')) {
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepends `count` repetitions of `indent` (default: one space) to each non-empty line of `string`.
|
|
3
|
+
* @example
|
|
4
|
+
* ```js
|
|
5
|
+
* import indentString from './lib/utils/indent-string.js';
|
|
6
|
+
* console.assert(indentString('hello\nworld', 2) === ' hello\n world');
|
|
7
|
+
* ```
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
1
10
|
export default function indentString(string, count = 1, options = {}) {
|
|
2
11
|
const { indent = ' ', includeEmptyLines = false } = options;
|
|
3
12
|
|
|
@@ -5,6 +5,10 @@ const targetInputs = {};
|
|
|
5
5
|
const inputs = [];
|
|
6
6
|
let listenerAdded = false;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Registers a stdin listener that fires `closure` when the user types `inputString` (case-insensitive by default).
|
|
10
|
+
* @returns {void}
|
|
11
|
+
*/
|
|
8
12
|
export default function listenToKeyboardKey(
|
|
9
13
|
inputString,
|
|
10
14
|
closure,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// { inputs: [], debug: true, watch: true, failFast: true, htmlPaths: [], output }
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Parses `process.argv` into a qunitx flag object (`inputs`, `debug`, `watch`, `failFast`, `timeout`, `output`, `port`, `before`, `after`).
|
|
4
|
+
* @returns {object}
|
|
5
|
+
*/
|
|
6
|
+
export default function parseCliFlags(projectRoot) {
|
|
3
7
|
const providedFlags = process.argv.slice(2).reduce(
|
|
4
8
|
(result, arg) => {
|
|
5
9
|
if (arg.startsWith('--debug')) {
|
package/lib/utils/path-exists.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Returns `true` if the given filesystem path is accessible, `false` otherwise.
|
|
5
|
+
* @example
|
|
6
|
+
* ```js
|
|
7
|
+
* import pathExists from './lib/utils/path-exists.js';
|
|
8
|
+
* console.assert(await pathExists('/tmp') === true);
|
|
9
|
+
* console.assert(await pathExists('/tmp/nonexistent-qunitx-file') === false);
|
|
10
|
+
* ```
|
|
11
|
+
* @returns {Promise<boolean>}
|
|
12
|
+
*/
|
|
3
13
|
export default async function pathExists(path) {
|
|
4
14
|
try {
|
|
5
15
|
await fs.access(path);
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { isSea, getAsset } from 'node:sea';
|
|
2
1
|
import fs from 'node:fs/promises';
|
|
3
2
|
import { dirname, join } from 'node:path';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
5
4
|
|
|
6
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Reads a boilerplate file by relative path, using the SEA asset store when running as a Node.js binary.
|
|
9
|
+
* @returns {Promise<string>}
|
|
10
|
+
*/
|
|
8
11
|
export default async function readBoilerplate(relativePath) {
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
const sea = await import('node:sea').catch(() => null);
|
|
13
|
+
if (sea?.isSea()) return sea.getAsset(relativePath, 'utf8');
|
|
14
|
+
return (await fs.readFile(join(__dirname, '../../templates', relativePath))).toString();
|
|
11
15
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns `portNumber` if it is free, otherwise recursively tries `portNumber + 1` until a free port is found.
|
|
3
|
+
* @returns {Promise<number>}
|
|
4
|
+
*/
|
|
1
5
|
export default async function resolvePortNumberFor(portNumber) {
|
|
2
6
|
if (await portIsAvailable(portNumber)) {
|
|
3
7
|
return portNumber;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Dynamically imports `modulePath` and calls its default export with `params`; exits with code 1 on error.
|
|
5
|
+
* @returns {Promise<void>}
|
|
6
|
+
*/
|
|
3
7
|
export default async function runUserModule(modulePath, params, scriptPosition) {
|
|
4
8
|
try {
|
|
5
9
|
const func = await import(modulePath);
|