qunitx-cli 0.1.1 → 0.5.0

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 (44) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.env +1 -0
  3. package/Makefile +35 -0
  4. package/README.md +120 -49
  5. package/cli.js +1 -1
  6. package/cliff.toml +23 -0
  7. package/demo/demo.gif +0 -0
  8. package/demo/demo.tape +59 -0
  9. package/demo/example-test.js +53 -0
  10. package/demo/failing-test.js +22 -0
  11. package/flake.lock +4 -4
  12. package/flake.nix +33 -4
  13. package/lib/boilerplates/default-project-config-values.js +2 -2
  14. package/lib/boilerplates/test.js +5 -4
  15. package/lib/commands/generate.js +6 -8
  16. package/lib/commands/help.js +8 -8
  17. package/lib/commands/init.js +35 -25
  18. package/lib/commands/run/tests-in-browser.js +97 -67
  19. package/lib/commands/run.js +165 -55
  20. package/lib/servers/http.js +53 -42
  21. package/lib/setup/bind-server-to-port.js +3 -12
  22. package/lib/setup/browser.js +26 -18
  23. package/lib/setup/config.js +8 -10
  24. package/lib/setup/file-watcher.js +23 -6
  25. package/lib/setup/fs-tree.js +29 -27
  26. package/lib/setup/keyboard-events.js +7 -4
  27. package/lib/setup/test-file-paths.js +25 -23
  28. package/lib/setup/web-server.js +87 -61
  29. package/lib/setup/write-output-static-files.js +4 -1
  30. package/lib/tap/display-final-result.js +2 -2
  31. package/lib/tap/display-test-result.js +32 -14
  32. package/lib/utils/find-chrome.js +16 -0
  33. package/lib/utils/find-internal-assets-from-html.js +7 -5
  34. package/lib/utils/find-project-root.js +1 -2
  35. package/lib/utils/indent-string.js +6 -6
  36. package/lib/utils/listen-to-keyboard-key.js +6 -2
  37. package/lib/utils/parse-cli-flags.js +34 -31
  38. package/lib/utils/resolve-port-number-for.js +3 -3
  39. package/lib/utils/run-user-module.js +5 -3
  40. package/lib/utils/search-in-parent-directories.js +4 -1
  41. package/lib/utils/time-counter.js +2 -2
  42. package/package.json +21 -36
  43. package/vendor/qunit.css +7 -7
  44. package/vendor/qunit.js +3772 -3324
@@ -3,14 +3,14 @@ import WebSocket, { WebSocketServer } from 'ws';
3
3
  import bindServerToPort from '../setup/bind-server-to-port.js';
4
4
 
5
5
  export const MIME_TYPES = {
6
- html: "text/html; charset=UTF-8",
7
- js: "application/javascript",
8
- css: "text/css",
9
- png: "image/png",
10
- jpg: "image/jpg",
11
- gif: "image/gif",
12
- ico: "image/x-icon",
13
- svg: "image/svg+xml",
6
+ html: 'text/html; charset=UTF-8',
7
+ js: 'application/javascript',
8
+ css: 'text/css',
9
+ png: 'image/png',
10
+ jpg: 'image/jpg',
11
+ gif: 'image/gif',
12
+ ico: 'image/x-icon',
13
+ svg: 'image/svg+xml',
14
14
  };
15
15
 
16
16
  export default class HTTPServer {
@@ -23,13 +23,15 @@ export default class HTTPServer {
23
23
  return handler(req, res);
24
24
  });
25
25
  server = server;
26
- server.on('error', (error) => {
27
- onError(error);
28
- reject(error);
29
- }).once('listening', () => {
30
- onListen(Object.assign({ hostname: '127.0.0.1', server }, config));
31
- resolve(server);
32
- })
26
+ server
27
+ .on('error', (error) => {
28
+ onError(error);
29
+ reject(error);
30
+ })
31
+ .once('listening', () => {
32
+ onListen(Object.assign({ hostname: '127.0.0.1', server }, config));
33
+ resolve(server);
34
+ });
33
35
 
34
36
  server.wss = new WebSocketServer({ server });
35
37
  server.wss.on('error', (error) => {
@@ -37,7 +39,7 @@ export default class HTTPServer {
37
39
  console.trace(error);
38
40
  });
39
41
 
40
- bindServerToPort(server, config)
42
+ bindServerToPort(server, config);
41
43
  });
42
44
  }
43
45
 
@@ -46,7 +48,7 @@ export default class HTTPServer {
46
48
  GET: {},
47
49
  POST: {},
48
50
  DELETE: {},
49
- PUT: {}
51
+ PUT: {},
50
52
  };
51
53
  this.middleware = [];
52
54
  this._server = http.createServer((req, res) => {
@@ -78,13 +80,17 @@ export default class HTTPServer {
78
80
 
79
81
  listen(port = 0, callback = () => {}) {
80
82
  return new Promise((resolve, reject) => {
81
- this._server.listen(port, (error) => {
82
- if (error) {
83
- reject(error);
84
- } else {
85
- resolve(callback());
86
- }
87
- });
83
+ const onError = (err) => {
84
+ this._server.off('listening', onListening);
85
+ reject(err);
86
+ };
87
+ const onListening = () => {
88
+ this._server.off('error', onError);
89
+ resolve(callback());
90
+ };
91
+ this._server.once('error', onError);
92
+ this._server.once('listening', onListening);
93
+ this._server.listen(port);
88
94
  });
89
95
  }
90
96
 
@@ -121,7 +127,7 @@ export default class HTTPServer {
121
127
  path,
122
128
  handler,
123
129
  paramNames: this.extractParamNames(path),
124
- isWildcard: path === '/*'
130
+ isWildcard: path === '/*',
125
131
  };
126
132
  }
127
133
 
@@ -159,27 +165,32 @@ export default class HTTPServer {
159
165
  return null;
160
166
  }
161
167
 
162
- return routes[url] || Object.values(routes).find(route => {
163
- const { path, isWildcard } = route;
168
+ return (
169
+ routes[url] ||
170
+ Object.values(routes).find((route) => {
171
+ const { path, isWildcard } = route;
164
172
 
165
- if (!isWildcard && !path.includes(':')) {
166
- return false;
167
- }
173
+ if (!isWildcard && !path.includes(':')) {
174
+ return false;
175
+ }
168
176
 
169
- if (isWildcard || this.matchPathSegments(path, url)) {
170
- if (route.paramNames.length > 0) {
171
- const regexPattern = this.buildRegexPattern(path, route.paramNames);
172
- const regex = new RegExp(`^${regexPattern}$`);
173
- const regexMatches = regex.exec(url);
174
- if (regexMatches) {
175
- route.paramValues = regexMatches.slice(1);
177
+ if (isWildcard || this.matchPathSegments(path, url)) {
178
+ if (route.paramNames.length > 0) {
179
+ const regexPattern = this.buildRegexPattern(path, route.paramNames);
180
+ const regex = new RegExp(`^${regexPattern}$`);
181
+ const regexMatches = regex.exec(url);
182
+ if (regexMatches) {
183
+ route.paramValues = regexMatches.slice(1);
184
+ }
176
185
  }
186
+ return true;
177
187
  }
178
- return true;
179
- }
180
188
 
181
- return false;
182
- }) || routes['/*'] || null;
189
+ return false;
190
+ }) ||
191
+ routes['/*'] ||
192
+ null
193
+ );
183
194
  }
184
195
 
185
196
  matchPathSegments(path, url) {
@@ -217,7 +228,7 @@ export default class HTTPServer {
217
228
  const paramRegex = /:(\w+)/g;
218
229
  const paramMatches = path.match(paramRegex);
219
230
 
220
- return paramMatches ? paramMatches.map(match => match.slice(1)) : [];
231
+ return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
221
232
  }
222
233
 
223
234
  extractParams(route, url) {
@@ -1,14 +1,5 @@
1
- import resolvePortNumberFor from '../utils/resolve-port-number-for.js';
2
-
3
- // NOTE: there was a race condition between socket.connection and server.listen, check if nanoexpress fixes it
4
1
  export default async function bindServerToPort(server, config) {
5
- try {
6
- let port = await resolvePortNumberFor(config.port);
7
-
8
- await server.listen(port);
9
-
10
- return server;
11
- } catch(e) {
12
- return await bindServerToPort(server, Object.assign(config, { port: config.port + 1 }));
13
- }
2
+ await server.listen(0);
3
+ config.port = server._server.address().port;
4
+ return server;
14
5
  }
@@ -1,23 +1,35 @@
1
1
  import Puppeteer from 'puppeteer';
2
2
  import setupWebServer from './web-server.js';
3
3
  import bindServerToPort from './bind-server-to-port.js';
4
+ import findChrome from '../utils/find-chrome.js';
4
5
 
5
- export default async function setupBrowser(config = {
6
- port: 1234, debug: false, watch: false, timeout: 10000
7
- }, cachedContent) {
6
+ export default async function setupBrowser(
7
+ config = {
8
+ port: 1234,
9
+ debug: false,
10
+ watch: false,
11
+ timeout: 10000,
12
+ },
13
+ cachedContent,
14
+ existingBrowser = null,
15
+ ) {
8
16
  let [server, browser] = await Promise.all([
9
17
  setupWebServer(config, cachedContent),
10
- Puppeteer.launch({
11
- debugger: config.debug || false,
12
- args: ['--no-sandbox', '--disable-gpu', '--remote-debugging-port=0', '--window-size=1440,900'],
13
- executablePath: process.env.CHROME_BIN || null,
14
- headless: 'new',
15
- }),
16
- ]);
17
- let [page, _] = await Promise.all([
18
- browser.newPage(),
19
- bindServerToPort(server, config)
18
+ existingBrowser
19
+ ? Promise.resolve(existingBrowser)
20
+ : Puppeteer.launch({
21
+ debugger: config.debug || false,
22
+ args: [
23
+ '--no-sandbox',
24
+ '--disable-gpu',
25
+ '--remote-debugging-port=0',
26
+ '--window-size=1440,900',
27
+ ],
28
+ executablePath: await findChrome(),
29
+ headless: true,
30
+ }),
20
31
  ]);
32
+ let [page, _] = await Promise.all([browser.newPage(), bindServerToPort(server, config)]);
21
33
 
22
34
  page.on('console', async (msg) => {
23
35
  if (config.debug) {
@@ -28,7 +40,7 @@ export default async function setupBrowser(config = {
28
40
  });
29
41
  page.on('error', (msg) => {
30
42
  try {
31
- throw error;
43
+ throw msg;
32
44
  } catch (e) {
33
45
  console.error(e, e.stack);
34
46
  console.log(e, e.stack);
@@ -49,7 +61,3 @@ export default async function setupBrowser(config = {
49
61
  function turnToObjects(jsHandle) {
50
62
  return jsHandle.jsonValue();
51
63
  }
52
-
53
- // function turnMStoSecond(timeInMS) {
54
- // return (timeInMS / 1000).toFixed(2);
55
- // }
@@ -7,23 +7,21 @@ import parseCliFlags from '../utils/parse-cli-flags.js';
7
7
 
8
8
  export default async function setupConfig() {
9
9
  let projectRoot = await findProjectRoot();
10
- let [projectPackageJSON, cliConfigFlags] = await Promise.all([
11
- readConfigFromPackageJSON(projectRoot),
12
- parseCliFlags(projectRoot)
13
- ]);
10
+ let cliConfigFlags = parseCliFlags(projectRoot);
11
+ let projectPackageJSON = await readConfigFromPackageJSON(projectRoot);
14
12
  let inputs = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
15
13
  let config = {
16
- projectRoot,
17
- htmlPaths: [],
18
- lastFailedTestFiles: null,
19
- lastRanTestFiles: null,
20
14
  ...defaultProjectConfigValues,
15
+ htmlPaths: [],
21
16
  ...projectPackageJSON.qunitx,
22
17
  ...cliConfigFlags,
23
- inputs
18
+ projectRoot,
19
+ inputs,
20
+ testFileLookupPaths: setupTestFilePaths(projectRoot, inputs),
21
+ lastFailedTestFiles: null,
22
+ lastRanTestFiles: null,
24
23
  };
25
24
  config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
26
- config.testFileLookupPaths = setupTestFilePaths(config.projectRoot, config.inputs);
27
25
  config.fsTree = await setupFSTree(config.testFileLookupPaths, config);
28
26
 
29
27
  return config;
@@ -1,7 +1,12 @@
1
1
  import chokidar from 'chokidar';
2
2
  import kleur from 'kleur';
3
3
 
4
- export default async function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
4
+ export default async function setupFileWatchers(
5
+ testFileLookupPaths,
6
+ config,
7
+ onEventFunc,
8
+ onFinishFunc,
9
+ ) {
5
10
  let extensions = ['js', 'ts'];
6
11
  let fileWatchers = testFileLookupPaths.reduce((watcher, watchPath) => {
7
12
  return Object.assign(watcher, {
@@ -9,14 +14,26 @@ export default async function setupFileWatchers(testFileLookupPaths, config, onE
9
14
  if (extensions.some((extension) => path.endsWith(extension))) {
10
15
  mutateFSTree(config.fsTree, event, path);
11
16
 
12
- console.log('#', kleur.magenta().bold('=================================================================='));
17
+ console.log(
18
+ '#',
19
+ kleur
20
+ .magenta()
21
+ .bold('=================================================================='),
22
+ );
13
23
  console.log('#', getEventColor(event), path.split(config.projectRoot)[1]);
14
- console.log('#', kleur.magenta().bold('=================================================================='));
24
+ console.log(
25
+ '#',
26
+ kleur
27
+ .magenta()
28
+ .bold('=================================================================='),
29
+ );
15
30
 
16
31
  if (!global.chokidarBuild) {
17
32
  global.chokidarBuild = true;
18
33
 
19
- let result = extensions.some((extension) => path.endsWith(extension)) ? onEventFunc(event, path) : null;
34
+ let result = extensions.some((extension) => path.endsWith(extension))
35
+ ? onEventFunc(event, path)
36
+ : null;
20
37
 
21
38
  if (!(result instanceof Promise)) {
22
39
  global.chokidarBuild = false;
@@ -35,7 +52,7 @@ export default async function setupFileWatchers(testFileLookupPaths, config, onE
35
52
  .finally(() => (global.chokidarBuild = false));
36
53
  }
37
54
  }
38
- })
55
+ }),
39
56
  });
40
57
  }, {});
41
58
 
@@ -45,7 +62,7 @@ export default async function setupFileWatchers(testFileLookupPaths, config, onE
45
62
  Object.keys(fileWatchers).forEach((watcherKey) => fileWatchers[watcherKey].close());
46
63
 
47
64
  return fileWatchers;
48
- }
65
+ },
49
66
  };
50
67
  }
51
68
 
@@ -6,43 +6,45 @@ export default async function buildFSTree(fileAbsolutePaths, config = {}) {
6
6
  let targetExtensions = ['js', 'ts'];
7
7
  let fsTree = {};
8
8
 
9
- await Promise.all(fileAbsolutePaths.map(async (fileAbsolutePath) => {
10
- let glob = picomatch.scan(fileAbsolutePath);
9
+ await Promise.all(
10
+ fileAbsolutePaths.map(async (fileAbsolutePath) => {
11
+ let glob = picomatch.scan(fileAbsolutePath);
11
12
 
12
- // TODO: maybe allow absolute path references
13
+ // TODO: maybe allow absolute path references
13
14
 
14
- try {
15
- if (glob.isGlob) {
16
- let fileNames = await recursiveLookup(glob.base, (path) => {
17
- return targetExtensions.some((extension) => path.endsWith(extension));
18
- });
19
-
20
- fileNames.forEach((fileName) => {
21
- if (picomatch.isMatch(fileName, fileAbsolutePath, { bash: true })) {
22
- fsTree[fileName] = null;
23
- }
24
- });
25
- } else {
26
- let entry = await fs.stat(fileAbsolutePath);
27
-
28
- if (entry.isFile()) {
29
- fsTree[fileAbsolutePath] = null;
30
- } else if (entry.isDirectory()) {
15
+ try {
16
+ if (glob.isGlob) {
31
17
  let fileNames = await recursiveLookup(glob.base, (path) => {
32
18
  return targetExtensions.some((extension) => path.endsWith(extension));
33
19
  });
34
20
 
35
21
  fileNames.forEach((fileName) => {
36
- fsTree[fileName] = null;
22
+ if (picomatch.isMatch(fileName, fileAbsolutePath, { bash: true })) {
23
+ fsTree[fileName] = null;
24
+ }
37
25
  });
26
+ } else {
27
+ let entry = await fs.stat(fileAbsolutePath);
28
+
29
+ if (entry.isFile()) {
30
+ fsTree[fileAbsolutePath] = null;
31
+ } else if (entry.isDirectory()) {
32
+ let fileNames = await recursiveLookup(glob.base, (path) => {
33
+ return targetExtensions.some((extension) => path.endsWith(extension));
34
+ });
35
+
36
+ fileNames.forEach((fileName) => {
37
+ fsTree[fileName] = null;
38
+ });
39
+ }
38
40
  }
39
- }
40
- } catch (error) {
41
- console.error(error);
41
+ } catch (error) {
42
+ console.error(error);
42
43
 
43
- return process.exit(1);
44
- }
45
- }));
44
+ return process.exit(1);
45
+ }
46
+ }),
47
+ );
46
48
 
47
49
  return fsTree;
48
50
  }
@@ -6,21 +6,24 @@ export default function setupKeyboardEvents(config, cachedContent, connections)
6
6
  listenToKeyboardKey('qq', () => abortBrowserQUnit(config, connections));
7
7
  listenToKeyboardKey('qa', () => {
8
8
  abortBrowserQUnit(config, connections);
9
- runTestsInBrowser(config, cachedContent, connections)
9
+ runTestsInBrowser(config, cachedContent, connections);
10
10
  });
11
11
  listenToKeyboardKey('qf', () => {
12
12
  abortBrowserQUnit(config, connections);
13
13
 
14
14
  if (!config.lastFailedTestFiles) {
15
- console.log('#', kleur.blue(`QUnitX: No tests failed so far, so repeating the last test run`));
15
+ console.log(
16
+ '#',
17
+ kleur.blue(`QUnitX: No tests failed so far, so repeating the last test run`),
18
+ );
16
19
  return runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles);
17
20
  }
18
21
 
19
- runTestsInBrowser(config, cachedContent, connections, config.lastFailedTestFiles)
22
+ runTestsInBrowser(config, cachedContent, connections, config.lastFailedTestFiles);
20
23
  });
21
24
  listenToKeyboardKey('ql', () => {
22
25
  abortBrowserQUnit(config, connections);
23
- runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles)
26
+ runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles);
24
27
  });
25
28
  }
26
29
 
@@ -1,25 +1,29 @@
1
1
  import picomatch from 'picomatch';
2
2
 
3
- export default function setupTestFilePaths(projectRoot, inputs) { // NOTE: very complex algorithm, order is very important
4
- let [folders, filesWithGlob, filesWithoutGlob] = inputs.reduce((result, input) => {
5
- let isGlob = picomatch.scan(input).isGlob;
3
+ export default function setupTestFilePaths(projectRoot, inputs) {
4
+ // NOTE: very complex algorithm, order is very important
5
+ let [folders, filesWithGlob, filesWithoutGlob] = inputs.reduce(
6
+ (result, input) => {
7
+ let isGlob = picomatch.scan(input).isGlob;
6
8
 
7
- if (!pathIsFile(input)) {
8
- result[0].push({
9
- input,
10
- isFile: false,
11
- isGlob
12
- });
13
- } else {
14
- result[isGlob ? 1 : 2].push({
15
- input,
16
- isFile: true,
17
- isGlob
18
- });
19
- }
9
+ if (!pathIsFile(input)) {
10
+ result[0].push({
11
+ input,
12
+ isFile: false,
13
+ isGlob,
14
+ });
15
+ } else {
16
+ result[isGlob ? 1 : 2].push({
17
+ input,
18
+ isFile: true,
19
+ isGlob,
20
+ });
21
+ }
20
22
 
21
- return result;
22
- }, [[], [], []]);
23
+ return result;
24
+ },
25
+ [[], [], []],
26
+ );
23
27
 
24
28
  let result = folders.reduce((folderResult, folder) => {
25
29
  if (!pathIsIncludedInPaths(folders, folder)) {
@@ -30,10 +34,7 @@ export default function setupTestFilePaths(projectRoot, inputs) { // NOTE: very
30
34
  }, []);
31
35
 
32
36
  filesWithGlob.forEach((file) => {
33
- if (
34
- !pathIsIncludedInPaths(result, file) &&
35
- !pathIsIncludedInPaths(filesWithGlob, file)
36
- ) {
37
+ if (!pathIsIncludedInPaths(result, file) && !pathIsIncludedInPaths(filesWithGlob, file)) {
37
38
  result.push(file);
38
39
  }
39
40
  });
@@ -68,7 +69,8 @@ function buildGlobFormat(path) {
68
69
  if (!path.isFile) {
69
70
  if (!path.isGlob) {
70
71
  return `${path.input}/*`;
71
- } else if (path.input.endsWith('*')) { // NOTE: could be problematic in future, investigate if I should check endsWith /*
72
+ } else if (path.input.endsWith('*')) {
73
+ // NOTE: could be problematic in future, investigate if I should check endsWith /*
72
74
  return path.input;
73
75
  }
74
76