qunitx-cli 0.5.4 → 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.
Files changed (39) hide show
  1. package/deno.json +12 -0
  2. package/deno.lock +1341 -0
  3. package/lib/commands/generate.js +7 -2
  4. package/lib/commands/help.js +2 -1
  5. package/lib/commands/init.js +3 -2
  6. package/lib/commands/run/tests-in-browser.js +12 -5
  7. package/lib/commands/run.js +17 -1
  8. package/lib/servers/http.js +40 -19
  9. package/lib/setup/bind-server-to-port.js +4 -0
  10. package/lib/setup/browser.js +16 -0
  11. package/lib/setup/config.js +5 -1
  12. package/lib/setup/default-project-config-values.js +7 -0
  13. package/lib/setup/file-watcher.js +4 -0
  14. package/lib/setup/fs-tree.js +6 -0
  15. package/lib/setup/keyboard-events.js +4 -0
  16. package/lib/setup/recursive-lookup.d.ts +7 -0
  17. package/lib/setup/test-file-paths.js +5 -0
  18. package/lib/setup/web-server.js +7 -7
  19. package/lib/setup/write-output-static-files.js +4 -0
  20. package/lib/tap/display-final-result.js +8 -1
  21. package/lib/tap/display-test-result.js +6 -1
  22. package/lib/utils/find-chrome.js +4 -0
  23. package/lib/utils/find-internal-assets-from-html.js +4 -0
  24. package/lib/utils/find-project-root.js +5 -1
  25. package/lib/utils/indent-string.js +9 -0
  26. package/lib/utils/listen-to-keyboard-key.js +4 -0
  27. package/lib/utils/parse-cli-flags.js +5 -1
  28. package/lib/utils/path-exists.js +10 -0
  29. package/lib/utils/read-boilerplate.js +7 -3
  30. package/lib/utils/resolve-port-number-for.js +4 -0
  31. package/lib/utils/run-user-module.js +4 -0
  32. package/lib/utils/search-in-parent-directories.js +4 -0
  33. package/lib/utils/time-counter.js +12 -1
  34. package/package.json +6 -2
  35. package/scripts/lint-docs.js +40 -0
  36. package/lib/boilerplates/default-project-config-values.js +0 -6
  37. /package/{lib/boilerplates → templates}/setup/tests.hbs +0 -0
  38. /package/{lib/boilerplates → templates}/setup/tsconfig.json +0 -0
  39. /package/{lib/boilerplates → templates}/test.js +0 -0
@@ -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
- export default async function () {
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
- return console.log(`${path} already exists!`);
20
+ console.log(`${path} already exists!`);
21
+ return;
17
22
  }
18
23
 
19
24
  const testJSContent = await readBoilerplate('test.js');
@@ -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
- export default function () {
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')}
@@ -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 '../boilerplates/default-project-config-values.js';
5
+ import defaultProjectConfigValues from '../setup/default-project-config-values.js';
6
6
  import readBoilerplate from '../utils/read-boilerplate.js';
7
7
 
8
- export default async function () {
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
- // Exported so run.js can pre-build all group bundles in parallel with Chrome startup.
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
 
@@ -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
- export default async function (config) {
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,
@@ -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.handleRequest(req, res);
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.registerRouteHandler('GET', path, handler);
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.registerRouteHandler('POST', path, handler);
123
+ this.#registerRouteHandler('POST', path, handler);
106
124
  }
107
125
 
126
+ /** Registers a DELETE route handler. */
108
127
  delete(path, handler) {
109
- this.registerRouteHandler('DELETE', path, handler);
128
+ this.#registerRouteHandler('DELETE', path, handler);
110
129
  }
111
130
 
131
+ /** Registers a PUT route handler. */
112
132
  put(path, handler) {
113
- this.registerRouteHandler('PUT', path, handler);
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.extractParamNames(path),
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.findRouteHandler(method, pathname);
160
+ const matchingRoute = this.#findRouteHandler(method, pathname);
140
161
 
141
162
  if (matchingRoute) {
142
- req.params = this.extractParams(matchingRoute, pathname);
143
- this.runMiddleware(req, res, matchingRoute.handler);
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.matchPathSegments(path, url)) {
201
+ if (isWildcard || this.#matchPathSegments(path, url)) {
181
202
  if (route.paramNames.length > 0) {
182
- const regexPattern = this.buildRegexPattern(path, route.paramNames);
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;
@@ -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,
@@ -1,10 +1,14 @@
1
1
  import fs from 'node:fs/promises';
2
- import defaultProjectConfigValues from '../boilerplates/default-project-config-values.js';
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);
@@ -0,0 +1,7 @@
1
+ /** Default qunitx config values: build output directory, test timeout (ms), fail-fast flag, and HTTP server port. */
2
+ export default {
3
+ output: 'tmp',
4
+ timeout: 20000,
5
+ failFast: false,
6
+ port: 1234,
7
+ };
@@ -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) => {
@@ -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', () => {
@@ -0,0 +1,7 @@
1
+ /** Recursively lists files under `folderPath`, optionally filtered by a predicate. */
2
+ declare function recursiveLookup(
3
+ folderPath: string,
4
+ filter?: (name: string) => boolean,
5
+ ): Promise<string[]>;
6
+
7
+ export default recursiveLookup;
@@ -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(
@@ -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.setTimeout(() => {
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.setTimeout(() => {
254
- window.testTimeout = ${config.timeout};
255
- }, 75);
255
+ window.testTimeout = ${config.timeout};
256
256
  });
257
257
 
258
- window.setTimeout(() => window.QUnit.start(), 25);
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
- export default function ({ testCount, passCount, skipCount, failCount }, timeTaken) {
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
- export default function (COUNTER, details) {
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
 
@@ -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
- export default async function () {
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
- export default function (projectRoot) {
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')) {
@@ -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
- if (isSea()) return getAsset(relativePath, 'utf8');
10
- return (await fs.readFile(join(__dirname, '../boilerplates', relativePath))).toString();
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);