qunitx-cli 0.0.2 → 0.0.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/Dockerfile +24 -0
  3. package/LICENSE +22 -0
  4. package/build.js +54 -1
  5. package/cli.js +24 -1
  6. package/lib/boilerplates/default-project-config-values.js +6 -0
  7. package/lib/boilerplates/setup/tests.hbs +15 -0
  8. package/lib/boilerplates/setup/tsconfig.json +109 -0
  9. package/lib/boilerplates/test.js +25 -0
  10. package/lib/commands/generate.js +33 -0
  11. package/lib/commands/help.js +38 -0
  12. package/lib/commands/init.js +70 -0
  13. package/lib/commands/run/tests-in-browser.js +162 -0
  14. package/lib/commands/run.js +119 -0
  15. package/lib/servers/http.js +233 -0
  16. package/lib/setup/bind-server-to-port.js +14 -0
  17. package/lib/setup/browser.js +55 -0
  18. package/lib/setup/config.js +46 -0
  19. package/lib/setup/file-watcher.js +72 -0
  20. package/lib/setup/fs-tree.js +48 -0
  21. package/lib/setup/keyboard-events.js +34 -0
  22. package/lib/setup/test-file-paths.js +79 -0
  23. package/lib/setup/web-server.js +241 -0
  24. package/lib/setup/write-output-static-files.js +22 -0
  25. package/lib/tap/display-final-result.js +15 -0
  26. package/lib/tap/display-test-result.js +73 -0
  27. package/lib/utils/find-internal-assets-from-html.js +16 -0
  28. package/lib/utils/find-project-root.js +17 -0
  29. package/lib/utils/indent-string.js +11 -0
  30. package/lib/utils/listen-to-keyboard-key.js +44 -0
  31. package/lib/utils/parse-cli-flags.js +57 -0
  32. package/lib/utils/path-exists.js +11 -0
  33. package/lib/utils/resolve-port-number-for.js +27 -0
  34. package/lib/utils/run-user-module.js +18 -0
  35. package/lib/utils/search-in-parent-directories.js +15 -0
  36. package/lib/utils/time-counter.js +8 -0
  37. package/package.json +1 -1
  38. package/test/commands/help-test.js +73 -0
  39. package/test/commands/index.js +2 -0
  40. package/test/commands/init-test.js +44 -0
  41. package/test/flags/after-test.js +23 -0
  42. package/test/flags/before-test.js +23 -0
  43. package/test/flags/coverage-test.js +6 -0
  44. package/test/flags/failfast-test.js +5 -0
  45. package/test/flags/index.js +2 -0
  46. package/test/flags/output-test.js +6 -0
  47. package/test/flags/reporter-test.js +6 -0
  48. package/test/flags/timeout-test.js +6 -0
  49. package/test/flags/watch-test.js +6 -0
  50. package/test/helpers/after-script-async.js +13 -0
  51. package/test/helpers/after-script-basic.js +1 -0
  52. package/test/helpers/assert-stdout.js +112 -0
  53. package/test/helpers/before-script-async.js +35 -0
  54. package/test/helpers/before-script-basic.js +1 -0
  55. package/test/helpers/before-script-web-server-tests.js +28 -0
  56. package/test/helpers/failing-tests.js +49 -0
  57. package/test/helpers/failing-tests.ts +49 -0
  58. package/test/helpers/fs-writers.js +36 -0
  59. package/test/helpers/index-with-content.html +20 -0
  60. package/test/helpers/index-without-content.html +22 -0
  61. package/test/helpers/passing-tests-dist.js +4883 -0
  62. package/test/helpers/passing-tests.js +44 -0
  63. package/test/helpers/passing-tests.ts +44 -0
  64. package/test/helpers/shell.js +37 -0
  65. package/test/index.js +22 -0
  66. package/test/inputs/advanced-htmls-test.js +21 -0
  67. package/test/inputs/error-edge-cases-test.js +11 -0
  68. package/test/inputs/file-and-folder-test.js +11 -0
  69. package/test/inputs/file-test.js +169 -0
  70. package/test/inputs/folder-test.js +193 -0
  71. package/test/inputs/index.js +5 -0
  72. package/test/setup/index.js +1 -0
  73. package/test/setup/test-file-paths-test.js +33 -0
  74. package/test/setup.js +17 -0
  75. package/vendor/package.json +1 -0
  76. package/vendor/qunit.css +525 -0
  77. package/vendor/qunit.js +7037 -0
@@ -0,0 +1,119 @@
1
+ import fs from 'node:fs/promises';
2
+ import { normalize, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import kleur from 'kleur';
5
+ import runTestsInBrowser from './run/tests-in-browser.js';
6
+ import setupBrowser from '../setup/browser.js';
7
+ import fileWatcher from '../setup/file-watcher.js';
8
+ import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.js';
9
+ import runUserModule from '../utils/run-user-module.js';
10
+ import setupKeyboardEvents from '../setup/keyboard-events.js';
11
+ import writeOutputStaticFiles from '../setup/write-output-static-files.js';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+
15
+ export default async function(config) {
16
+ let cachedContent = await buildCachedContent(config, config.htmlPaths);
17
+ let [connections, _] = await Promise.all([
18
+ setupBrowser(config, cachedContent),
19
+ writeOutputStaticFiles(config, cachedContent)
20
+ ]);
21
+ config.expressApp = connections.server;
22
+
23
+ if (config.watch) {
24
+ setupKeyboardEvents(config, cachedContent, connections);
25
+ }
26
+
27
+ if (config.before) {
28
+ await runUserModule(`${process.cwd()}/${config.before}`, config, 'before');
29
+ }
30
+
31
+ await runTestsInBrowser(config, cachedContent, connections);
32
+
33
+ if (config.watch) {
34
+ logWatcherAndKeyboardShortcutInfo(config, connections.server);
35
+
36
+ await fileWatcher(
37
+ config.testFileLookupPaths,
38
+ config,
39
+ async (event, file) => {
40
+ if (event === 'addDir') {
41
+ return;
42
+ } else if (['unlink', 'unlinkDir'].includes(event)) {
43
+ return await runTestsInBrowser(config, cachedContent, connections);
44
+ }
45
+
46
+ await runTestsInBrowser(config, cachedContent, connections, [file]);
47
+ },
48
+ (path, event) => connections.server.publish('refresh', 'refresh')
49
+ );
50
+ }
51
+ }
52
+
53
+ async function buildCachedContent(config, htmlPaths) {
54
+ let htmlBuffers = await Promise.all(config.htmlPaths.map((htmlPath) => fs.readFile(htmlPath))); // TODO: remove this and read it from the fsTree, should be cached?
55
+ let cachedContent = htmlPaths.reduce((result, htmlPath, index) => {
56
+ let filePath = config.htmlPaths[index];
57
+ let html = htmlBuffers[index].toString();
58
+
59
+ if (html.includes('{{content}}')) { // TODO: here I could do html analysis to see which static js certain html points to? Complex algorithm
60
+ result.dynamicContentHTMLs[filePath] = html;
61
+
62
+ let relativePath = filePath.replace(config.projectRoot, '');
63
+
64
+ result.htmlPathsToRunTests.push(relativePath);
65
+ } else {
66
+ console.log('#', kleur.yellow(`WARNING: Static html file with no {{content}} detected. Therefore ignoring ${filePath}`));
67
+ result.staticHTMLs[filePath] = html;
68
+ }
69
+
70
+ findInternalAssetsFromHTML(html).forEach((key) => {
71
+ result.assets.add(normalizeInternalAssetPathFromHTML(config.projectRoot, key, filePath))
72
+ });
73
+
74
+ return result;
75
+ }, {
76
+ allTestCode: null,
77
+ assets: new Set(),
78
+ htmlPathsToRunTests: [],
79
+ mainHTML: { filePath: null, html: null },
80
+ staticHTMLs: {},
81
+ dynamicContentHTMLs: {}
82
+ });
83
+
84
+ if (cachedContent.htmlPathsToRunTests.length === 0) {
85
+ cachedContent.htmlPathsToRunTests = ['/'];
86
+ }
87
+
88
+ return await addCachedContentMainHTML(config.projectRoot, cachedContent);
89
+ }
90
+
91
+ async function addCachedContentMainHTML(projectRoot, cachedContent) {
92
+ let mainHTMLPath = Object.keys(cachedContent.dynamicContentHTMLs)[0];
93
+ if (mainHTMLPath) {
94
+ cachedContent.mainHTML = {
95
+ filePath: mainHTMLPath,
96
+ html: cachedContent.dynamicContentHTMLs[mainHTMLPath]
97
+ };
98
+ } else {
99
+ let html = (await fs.readFile(`${__dirname}/../boilerplates/setup/tests.hbs`)).toString();
100
+
101
+ cachedContent.mainHTML = { filePath: `${projectRoot}/test/tests.html`, html };
102
+ cachedContent.assets.add(`${projectRoot}/node_modules/qunitx/vendor/qunit.css`);
103
+ }
104
+
105
+ return cachedContent;
106
+ }
107
+
108
+ function logWatcherAndKeyboardShortcutInfo(config, server) {
109
+ console.log('#', kleur.blue(`Watching files... You can browse the tests on http://localhost:${config.port} ...`)); // NOTE: maybe add also qx to exit
110
+ console.log('#', kleur.blue(`Shortcuts: Press "qq" to abort running tests, "qa" to run all the tests, "qf" to run last failing test, "ql" to repeat last test`)); // NOTE: maybe add also qx to test specific
111
+ }
112
+
113
+ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) { // NOTE: maybe normalize ..
114
+ let currentDirectory = htmlPath ? htmlPath.split('/').slice(0, -1).join('/') : projectRoot;
115
+
116
+ return assetPath.startsWith('./')
117
+ ? normalize(`${currentDirectory}/${assetPath.slice(2)}`)
118
+ : normalize(`${currentDirectory}/${assetPath}`);
119
+ }
@@ -0,0 +1,233 @@
1
+ import http from 'node:http';
2
+ import WebSocket, { WebSocketServer } from 'ws';
3
+ import bindServerToPort from '../setup/bind-server-to-port.js';
4
+
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",
14
+ };
15
+
16
+ export default class HTTPServer {
17
+ static serve(config = { port: 1234 }, handler) {
18
+ let onListen = config.onListen || ((server) => {});
19
+ let onError = config.onError || ((error) => {});
20
+
21
+ return new Promise((resolve, reject) => {
22
+ let server = http.createServer((req, res) => {
23
+ return handler(req, res);
24
+ });
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
+ })
33
+
34
+ server.wss = new WebSocketServer({ server });
35
+ server.wss.on('error', (error) => {
36
+ console.log('# [WebSocketServer] Error:');
37
+ console.trace(error);
38
+ });
39
+
40
+ bindServerToPort(server, config)
41
+ });
42
+ }
43
+
44
+ constructor() {
45
+ this.routes = {
46
+ GET: {},
47
+ POST: {},
48
+ DELETE: {},
49
+ PUT: {}
50
+ };
51
+ this.middleware = [];
52
+ this._server = http.createServer((req, res) => {
53
+ res.send = (data) => {
54
+ res.setHeader('Content-Type', 'text/plain');
55
+ res.end(data);
56
+ };
57
+ res.json = (data) => {
58
+ res.setHeader('Content-Type', 'application/json');
59
+ res.end(JSON.stringify(data));
60
+ };
61
+
62
+ return this.handleRequest(req, res);
63
+ });
64
+ this.wss = new WebSocketServer({ server: this._server });
65
+ this.wss.on('error', (error) => {
66
+ console.log('# [WebSocketServer] Error:');
67
+ console.log(error);
68
+ });
69
+ }
70
+
71
+ close() {
72
+ return this._server.close();
73
+ }
74
+
75
+ get(path, handler) {
76
+ this.registerRouteHandler('GET', path, handler);
77
+ }
78
+
79
+ listen(port = 0, callback = () => {}) {
80
+ return new Promise((resolve, reject) => {
81
+ this._server.listen(port, (error) => {
82
+ if (error) {
83
+ reject(error);
84
+ } else {
85
+ resolve(callback());
86
+ }
87
+ });
88
+ });
89
+ }
90
+
91
+ publish(data) {
92
+ this.wss.clients.forEach((client) => {
93
+ if (client.readyState === WebSocket.OPEN) {
94
+ client.send(data);
95
+ }
96
+ });
97
+ }
98
+
99
+ post(path, handler) {
100
+ this.registerRouteHandler('POST', path, handler);
101
+ }
102
+
103
+ delete(path, handler) {
104
+ this.registerRouteHandler('DELETE', path, handler);
105
+ }
106
+
107
+ put(path, handler) {
108
+ this.registerRouteHandler('PUT', path, handler);
109
+ }
110
+
111
+ use(middleware) {
112
+ this.middleware.push(middleware);
113
+ }
114
+
115
+ registerRouteHandler(method, path, handler) {
116
+ if (!this.routes[method]) {
117
+ this.routes[method] = {};
118
+ }
119
+
120
+ this.routes[method][path] = {
121
+ path,
122
+ handler,
123
+ paramNames: this.extractParamNames(path),
124
+ isWildcard: path === '/*'
125
+ };
126
+ }
127
+
128
+ handleRequest(req, res) {
129
+ const { method, url } = req;
130
+ const matchingRoute = this.findRouteHandler(method, url);
131
+
132
+ if (matchingRoute) {
133
+ req.params = this.extractParams(matchingRoute, url);
134
+ this.runMiddleware(req, res, matchingRoute.handler);
135
+ } else {
136
+ res.statusCode = 404;
137
+ res.setHeader('Content-Type', 'text/plain');
138
+ res.end('Not found');
139
+ }
140
+ }
141
+
142
+ runMiddleware(req, res, callback) {
143
+ let index = 0;
144
+ const next = () => {
145
+ if (index >= this.middleware.length) {
146
+ callback(req, res);
147
+ } else {
148
+ const middleware = this.middleware[index];
149
+ index++;
150
+ middleware(req, res, next);
151
+ }
152
+ };
153
+ next();
154
+ }
155
+
156
+ findRouteHandler(method, url) {
157
+ const routes = this.routes[method];
158
+ if (!routes) {
159
+ return null;
160
+ }
161
+
162
+ return routes[url] || Object.values(routes).find(route => {
163
+ const { path, isWildcard } = route;
164
+
165
+ if (!isWildcard && !path.includes(':')) {
166
+ return false;
167
+ }
168
+
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);
176
+ }
177
+ }
178
+ return true;
179
+ }
180
+
181
+ return false;
182
+ }) || routes['/*'] || null;
183
+ }
184
+
185
+ matchPathSegments(path, url) {
186
+ const pathSegments = path.split('/');
187
+ const urlSegments = url.split('/');
188
+
189
+ if (pathSegments.length !== urlSegments.length) {
190
+ return false;
191
+ }
192
+
193
+ for (let i = 0; i < pathSegments.length; i++) {
194
+ const pathSegment = pathSegments[i];
195
+ const urlSegment = urlSegments[i];
196
+
197
+ if (pathSegment.startsWith(':')) {
198
+ continue;
199
+ }
200
+
201
+ if (pathSegment !== urlSegment) {
202
+ return false;
203
+ }
204
+ }
205
+
206
+ return true;
207
+ }
208
+
209
+ buildRegexPattern(path, paramNames) {
210
+ let regexPattern = path.replace(/:[^/]+/g, '([^/]+)');
211
+ regexPattern = regexPattern.replace(/\//g, '\\/');
212
+
213
+ return regexPattern;
214
+ }
215
+
216
+ extractParamNames(path) {
217
+ const paramRegex = /:(\w+)/g;
218
+ const paramMatches = path.match(paramRegex);
219
+
220
+ return paramMatches ? paramMatches.map(match => match.slice(1)) : [];
221
+ }
222
+
223
+ extractParams(route, url) {
224
+ const { paramNames, paramValues } = route;
225
+ const params = {};
226
+
227
+ for (let i = 0; i < paramNames.length; i++) {
228
+ params[paramNames[i]] = paramValues[i];
229
+ }
230
+
231
+ return params;
232
+ }
233
+ }
@@ -0,0 +1,14 @@
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
+ 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
+ }
14
+ }
@@ -0,0 +1,55 @@
1
+ import Puppeteer from 'puppeteer';
2
+ import setupWebServer from './web-server.js';
3
+ import bindServerToPort from './bind-server-to-port.js';
4
+
5
+ export default async function setupBrowser(config = {
6
+ port: 1234, debug: false, watch: false, timeout: 10000
7
+ }, cachedContent) {
8
+ let [server, browser] = await Promise.all([
9
+ 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)
20
+ ]);
21
+
22
+ page.on('console', async (msg) => {
23
+ if (config.debug) {
24
+ const args = await Promise.all(msg.args().map((arg) => turnToObjects(arg)));
25
+
26
+ console.log(...args);
27
+ }
28
+ });
29
+ page.on('error', (msg) => {
30
+ try {
31
+ throw error;
32
+ } catch (e) {
33
+ console.error(e, e.stack);
34
+ console.log(e, e.stack);
35
+ }
36
+ });
37
+ page.on('pageerror', async (error) => {
38
+ try {
39
+ throw error;
40
+ } catch (e) {
41
+ console.log(e.toString());
42
+ console.error(e.toString());
43
+ }
44
+ });
45
+
46
+ return { server, browser, page };
47
+ }
48
+
49
+ function turnToObjects(jsHandle) {
50
+ return jsHandle.jsonValue();
51
+ }
52
+
53
+ // function turnMStoSecond(timeInMS) {
54
+ // return (timeInMS / 1000).toFixed(2);
55
+ // }
@@ -0,0 +1,46 @@
1
+ import fs from 'node:fs/promises';
2
+ import defaultProjectConfigValues from '../boilerplates/default-project-config-values.js';
3
+ import findProjectRoot from '../utils/find-project-root.js';
4
+ import setupFSTree from './fs-tree.js';
5
+ import setupTestFilePaths from './test-file-paths.js';
6
+ import parseCliFlags from '../utils/parse-cli-flags.js';
7
+
8
+ export default async function setupConfig() {
9
+ let projectRoot = await findProjectRoot();
10
+ let [projectPackageJSON, cliConfigFlags] = await Promise.all([
11
+ readConfigFromPackageJSON(projectRoot),
12
+ parseCliFlags(projectRoot)
13
+ ]);
14
+ let inputs = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
15
+ let config = {
16
+ projectRoot,
17
+ htmlPaths: [],
18
+ lastFailedTestFiles: null,
19
+ lastRanTestFiles: null,
20
+ ...defaultProjectConfigValues,
21
+ ...projectPackageJSON.qunitx,
22
+ ...cliConfigFlags,
23
+ inputs
24
+ };
25
+ config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
26
+ config.testFileLookupPaths = setupTestFilePaths(config.projectRoot, config.inputs);
27
+ config.fsTree = await setupFSTree(config.testFileLookupPaths, config);
28
+
29
+ return config;
30
+ }
31
+
32
+ async function readConfigFromPackageJSON(projectRoot) {
33
+ let packageJSON = await fs.readFile(`${projectRoot}/package.json`);
34
+
35
+ return JSON.parse(packageJSON.toString());
36
+ }
37
+
38
+ function normalizeHTMLPaths(projectRoot, htmlPaths) {
39
+ return Array.from(new Set(htmlPaths.map((htmlPath) => `${projectRoot}/${htmlPath}`)));
40
+ }
41
+
42
+ function readInputsFromPackageJSON(packageJSON) {
43
+ let qunitx = packageJSON.qunitx;
44
+
45
+ return qunitx && qunitx.inputs ? qunitx.inputs : [];
46
+ }
@@ -0,0 +1,72 @@
1
+ import chokidar from 'chokidar';
2
+ import kleur from 'kleur';
3
+
4
+ export default async function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
5
+ let extensions = ['js', 'ts'];
6
+ let fileWatchers = testFileLookupPaths.reduce((watcher, watchPath) => {
7
+ return Object.assign(watcher, {
8
+ [watchPath]: chokidar.watch(watchPath, { ignoreInitial: true }).on('all', (event, path) => {
9
+ if (extensions.some((extension) => path.endsWith(extension))) {
10
+ mutateFSTree(config.fsTree, event, path);
11
+
12
+ console.log('#', kleur.magenta().bold('=================================================================='));
13
+ console.log('#', getEventColor(event), path.split(config.projectRoot)[1]);
14
+ console.log('#', kleur.magenta().bold('=================================================================='));
15
+
16
+ if (!global.chokidarBuild) {
17
+ global.chokidarBuild = true;
18
+
19
+ let result = extensions.some((extension) => path.endsWith(extension)) ? onEventFunc(event, path) : null;
20
+
21
+ if (!(result instanceof Promise)) {
22
+ global.chokidarBuild = false;
23
+
24
+ return result;
25
+ }
26
+
27
+ result
28
+ .then(() => {
29
+ onFinishFunc ? onFinishFunc(event, path) : null;
30
+ })
31
+ .catch(() => {
32
+ // TODO: make an index.html to display the error
33
+ // error type has to be derived from the error!
34
+ })
35
+ .finally(() => (global.chokidarBuild = false));
36
+ }
37
+ }
38
+ })
39
+ });
40
+ }, {});
41
+
42
+ return {
43
+ fileWatchers,
44
+ killFileWatchers() {
45
+ Object.keys(fileWatchers).forEach((watcherKey) => fileWatchers[watcherKey].close());
46
+
47
+ return fileWatchers;
48
+ }
49
+ };
50
+ }
51
+
52
+ function mutateFSTree(fsTree, event, path) {
53
+ if (event === 'add') {
54
+ fsTree[path] = null;
55
+ } else if (event === 'unlink') {
56
+ delete fsTree[path];
57
+ } else if (event === 'unlinkDir') {
58
+ let targetPaths = Object.keys(config.fsTree).filter((treePath) => treePath.startsWith(path));
59
+
60
+ targetPaths.forEach((path) => delete config.fsTree[path]);
61
+ }
62
+ }
63
+
64
+ function getEventColor(event) {
65
+ if (event === 'change') {
66
+ return kleur.yellow('CHANGED:');
67
+ } else if (event === 'add' || event === 'addDir') {
68
+ return kleur.green('ADDED:');
69
+ } else if (event === 'unlink' || event === 'unlinkDir') {
70
+ return kleur.red('REMOVED:');
71
+ }
72
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs/promises';
2
+ import picomatch from 'picomatch';
3
+ import recursiveLookup from 'recursive-lookup';
4
+
5
+ export default async function buildFSTree(fileAbsolutePaths, config = {}) {
6
+ let targetExtensions = ['js', 'ts'];
7
+ let fsTree = {};
8
+
9
+ await Promise.all(fileAbsolutePaths.map(async (fileAbsolutePath) => {
10
+ let glob = picomatch.scan(fileAbsolutePath);
11
+
12
+ // TODO: maybe allow absolute path references
13
+
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()) {
31
+ let fileNames = await recursiveLookup(glob.base, (path) => {
32
+ return targetExtensions.some((extension) => path.endsWith(extension));
33
+ });
34
+
35
+ fileNames.forEach((fileName) => {
36
+ fsTree[fileName] = null;
37
+ });
38
+ }
39
+ }
40
+ } catch (error) {
41
+ console.error(error);
42
+
43
+ return process.exit(1);
44
+ }
45
+ }));
46
+
47
+ return fsTree;
48
+ }
@@ -0,0 +1,34 @@
1
+ import kleur from 'kleur';
2
+ import listenToKeyboardKey from '../utils/listen-to-keyboard-key.js';
3
+ import runTestsInBrowser from '../commands/run/tests-in-browser.js';
4
+
5
+ export default function setupKeyboardEvents(config, cachedContent, connections) {
6
+ listenToKeyboardKey('qq', () => abortBrowserQUnit(config, connections));
7
+ listenToKeyboardKey('qa', () => {
8
+ abortBrowserQUnit(config, connections);
9
+ runTestsInBrowser(config, cachedContent, connections)
10
+ });
11
+ listenToKeyboardKey('qf', () => {
12
+ abortBrowserQUnit(config, connections);
13
+
14
+ if (!config.lastFailedTestFiles) {
15
+ console.log('#', kleur.blue(`QUnitX: No tests failed so far, so repeating the last test run`));
16
+ return runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles);
17
+ }
18
+
19
+ runTestsInBrowser(config, cachedContent, connections, config.lastFailedTestFiles)
20
+ });
21
+ listenToKeyboardKey('ql', () => {
22
+ abortBrowserQUnit(config, connections);
23
+ runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles)
24
+ });
25
+ }
26
+
27
+ function abortBrowserQUnit(config, connections) {
28
+ connections.server.publish('abort', 'abort');
29
+ }
30
+
31
+ function abortNodejsQUnit(config) {
32
+ window.QUnit.config.queue.length = 0;
33
+ config.aborted = true;
34
+ }
@@ -0,0 +1,79 @@
1
+ import picomatch from 'picomatch';
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;
6
+
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
+ }
20
+
21
+ return result;
22
+ }, [[], [], []]);
23
+
24
+ let result = folders.reduce((folderResult, folder) => {
25
+ if (!pathIsIncludedInPaths(folders, folder)) {
26
+ folderResult.push(folder);
27
+ }
28
+
29
+ return folderResult;
30
+ }, []);
31
+
32
+ filesWithGlob.forEach((file) => {
33
+ if (
34
+ !pathIsIncludedInPaths(result, file) &&
35
+ !pathIsIncludedInPaths(filesWithGlob, file)
36
+ ) {
37
+ result.push(file);
38
+ }
39
+ });
40
+ filesWithoutGlob.forEach((file) => {
41
+ if (!pathIsIncludedInPaths(result, file)) {
42
+ result.push(file);
43
+ }
44
+ });
45
+
46
+ return result.map((metaItem) => metaItem.input);
47
+ }
48
+
49
+ function pathIsFile(path) {
50
+ let inputs = path.split('/');
51
+
52
+ return inputs[inputs.length - 1].includes('.');
53
+ }
54
+
55
+ function pathIsIncludedInPaths(paths, targetPath) {
56
+ return paths.some((path) => {
57
+ if (path === targetPath) {
58
+ return false;
59
+ }
60
+
61
+ let globFormat = buildGlobFormat(path);
62
+
63
+ return picomatch.isMatch(targetPath.input, globFormat, { bash: true });
64
+ });
65
+ }
66
+
67
+ function buildGlobFormat(path) {
68
+ if (!path.isFile) {
69
+ if (!path.isGlob) {
70
+ return `${path.input}/*`;
71
+ } else if (path.input.endsWith('*')) { // NOTE: could be problematic in future, investigate if I should check endsWith /*
72
+ return path.input;
73
+ }
74
+
75
+ return `${path.input}/*`;
76
+ }
77
+
78
+ return path.input;
79
+ }