qunitx-cli 0.1.2 → 0.5.1
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/README.md +120 -49
- package/cli.js +1 -1
- package/lib/boilerplates/default-project-config-values.js +2 -2
- package/lib/boilerplates/test.js +5 -4
- package/lib/commands/generate.js +6 -8
- package/lib/commands/help.js +8 -8
- package/lib/commands/init.js +35 -25
- package/lib/commands/run/tests-in-browser.js +97 -67
- package/lib/commands/run.js +165 -55
- package/lib/servers/http.js +59 -44
- package/lib/setup/bind-server-to-port.js +3 -12
- package/lib/setup/browser.js +26 -18
- package/lib/setup/config.js +8 -10
- package/lib/setup/file-watcher.js +23 -6
- package/lib/setup/fs-tree.js +29 -27
- package/lib/setup/keyboard-events.js +7 -4
- package/lib/setup/test-file-paths.js +25 -23
- package/lib/setup/web-server.js +87 -61
- package/lib/setup/write-output-static-files.js +4 -1
- package/lib/tap/display-final-result.js +2 -2
- package/lib/tap/display-test-result.js +32 -14
- package/lib/utils/find-chrome.js +16 -0
- package/lib/utils/find-internal-assets-from-html.js +7 -5
- package/lib/utils/find-project-root.js +1 -2
- package/lib/utils/indent-string.js +6 -6
- package/lib/utils/listen-to-keyboard-key.js +6 -2
- package/lib/utils/parse-cli-flags.js +34 -31
- package/lib/utils/resolve-port-number-for.js +3 -3
- package/lib/utils/run-user-module.js +5 -3
- package/lib/utils/search-in-parent-directories.js +4 -1
- package/lib/utils/time-counter.js +2 -2
- package/package.json +21 -35
- package/vendor/qunit.css +7 -7
- package/vendor/qunit.js +3772 -3324
- package/flake.lock +0 -64
- package/flake.nix +0 -26
package/lib/commands/run.js
CHANGED
|
@@ -1,103 +1,200 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import { normalize, dirname } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { availableParallelism } from 'node:os';
|
|
5
|
+
import Puppeteer from 'puppeteer';
|
|
4
6
|
import kleur from 'kleur';
|
|
5
|
-
import runTestsInBrowser from './run/tests-in-browser.js';
|
|
7
|
+
import runTestsInBrowser, { buildTestBundle } from './run/tests-in-browser.js';
|
|
6
8
|
import setupBrowser from '../setup/browser.js';
|
|
7
9
|
import fileWatcher from '../setup/file-watcher.js';
|
|
8
10
|
import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.js';
|
|
9
11
|
import runUserModule from '../utils/run-user-module.js';
|
|
10
12
|
import setupKeyboardEvents from '../setup/keyboard-events.js';
|
|
11
13
|
import writeOutputStaticFiles from '../setup/write-output-static-files.js';
|
|
14
|
+
import timeCounter from '../utils/time-counter.js';
|
|
15
|
+
import TAPDisplayFinalResult from '../tap/display-final-result.js';
|
|
16
|
+
import findChrome from '../utils/find-chrome.js';
|
|
12
17
|
|
|
13
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
19
|
|
|
15
|
-
export default async function(config) {
|
|
16
|
-
|
|
17
|
-
let [connections, _] = await Promise.all([
|
|
18
|
-
setupBrowser(config, cachedContent),
|
|
19
|
-
writeOutputStaticFiles(config, cachedContent)
|
|
20
|
-
]);
|
|
21
|
-
config.expressApp = connections.server;
|
|
20
|
+
export default async function (config) {
|
|
21
|
+
const cachedContent = await buildCachedContent(config, config.htmlPaths);
|
|
22
22
|
|
|
23
23
|
if (config.watch) {
|
|
24
|
+
// WATCH MODE: single browser, all test files bundled together.
|
|
25
|
+
// The HTTP server stays alive so the user can browse http://localhost:PORT
|
|
26
|
+
// and see all tests running in a single QUnit view.
|
|
27
|
+
const [connections] = await Promise.all([
|
|
28
|
+
setupBrowser(config, cachedContent),
|
|
29
|
+
writeOutputStaticFiles(config, cachedContent),
|
|
30
|
+
]);
|
|
31
|
+
config.expressApp = connections.server;
|
|
24
32
|
setupKeyboardEvents(config, cachedContent, connections);
|
|
25
|
-
}
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
if (config.before) {
|
|
35
|
+
await runUserModule(`${process.cwd()}/${config.before}`, config, 'before');
|
|
36
|
+
}
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
try {
|
|
39
|
+
await runTestsInBrowser(config, cachedContent, connections);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
await Promise.all([
|
|
42
|
+
connections.server && connections.server.close(),
|
|
43
|
+
connections.browser && connections.browser.close(),
|
|
44
|
+
]);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
32
47
|
|
|
33
|
-
if (config.watch) {
|
|
34
48
|
logWatcherAndKeyboardShortcutInfo(config, connections.server);
|
|
35
49
|
|
|
36
50
|
await fileWatcher(
|
|
37
51
|
config.testFileLookupPaths,
|
|
38
52
|
config,
|
|
39
53
|
async (event, file) => {
|
|
40
|
-
if (event === 'addDir')
|
|
41
|
-
|
|
42
|
-
} else if (['unlink', 'unlinkDir'].includes(event)) {
|
|
54
|
+
if (event === 'addDir') return;
|
|
55
|
+
if (['unlink', 'unlinkDir'].includes(event)) {
|
|
43
56
|
return await runTestsInBrowser(config, cachedContent, connections);
|
|
44
57
|
}
|
|
45
|
-
|
|
46
58
|
await runTestsInBrowser(config, cachedContent, connections, [file]);
|
|
47
59
|
},
|
|
48
|
-
(
|
|
60
|
+
(_path, _event) => connections.server.publish('refresh', 'refresh'),
|
|
49
61
|
);
|
|
50
|
-
}
|
|
51
|
-
|
|
62
|
+
} else {
|
|
63
|
+
// CONCURRENT MODE: split test files across N groups = availableParallelism().
|
|
64
|
+
// All group bundles are built while Chrome is starting up, so esbuild time
|
|
65
|
+
// is hidden behind the ~1.2s Chrome launch. Each group then gets its own
|
|
66
|
+
// HTTP server and Puppeteer page inside one shared browser instance.
|
|
67
|
+
const allFiles = Object.keys(config.fsTree);
|
|
68
|
+
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
69
|
+
const groups = splitIntoGroups(allFiles, groupCount);
|
|
70
|
+
|
|
71
|
+
// Shared COUNTER so TAP test numbers are globally sequential across all groups.
|
|
72
|
+
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 };
|
|
73
|
+
config.lastRanTestFiles = allFiles;
|
|
74
|
+
|
|
75
|
+
const groupConfigs = groups.map((groupFiles, i) => ({
|
|
76
|
+
...config,
|
|
77
|
+
fsTree: Object.fromEntries(groupFiles.map((f) => [f, config.fsTree[f]])),
|
|
78
|
+
// Single group keeps the root output dir for backward-compatible file paths.
|
|
79
|
+
output: groupCount === 1 ? config.output : `${config.output}/group-${i}`,
|
|
80
|
+
_groupMode: true,
|
|
81
|
+
}));
|
|
82
|
+
const groupCachedContents = groups.map(() => ({ ...cachedContent }));
|
|
83
|
+
|
|
84
|
+
// Build all group bundles and write static files while Chrome is starting up.
|
|
85
|
+
const [browser] = await Promise.all([
|
|
86
|
+
findChrome().then((chromePath) =>
|
|
87
|
+
Puppeteer.launch({
|
|
88
|
+
args: [
|
|
89
|
+
'--no-sandbox',
|
|
90
|
+
'--disable-gpu',
|
|
91
|
+
'--remote-debugging-port=0',
|
|
92
|
+
'--window-size=1440,900',
|
|
93
|
+
],
|
|
94
|
+
executablePath: chromePath,
|
|
95
|
+
headless: true,
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
Promise.all(
|
|
99
|
+
groupConfigs.map((groupConfig, i) =>
|
|
100
|
+
Promise.all([
|
|
101
|
+
buildTestBundle(groupConfig, groupCachedContents[i]),
|
|
102
|
+
writeOutputStaticFiles(groupConfig, groupCachedContents[i]),
|
|
103
|
+
]),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
console.log('TAP version 13');
|
|
109
|
+
const TIME_COUNTER = timeCounter();
|
|
110
|
+
let hasFatalError = false;
|
|
111
|
+
|
|
112
|
+
await Promise.allSettled(
|
|
113
|
+
groupConfigs.map(async (groupConfig, i) => {
|
|
114
|
+
const connections = await setupBrowser(groupConfig, groupCachedContents[i], browser);
|
|
115
|
+
groupConfig.expressApp = connections.server;
|
|
116
|
+
|
|
117
|
+
if (config.before) {
|
|
118
|
+
await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, 'before');
|
|
119
|
+
}
|
|
52
120
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
121
|
+
try {
|
|
122
|
+
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
123
|
+
} catch {
|
|
124
|
+
hasFatalError = true;
|
|
125
|
+
} finally {
|
|
126
|
+
await Promise.all([
|
|
127
|
+
connections.server && connections.server.close(),
|
|
128
|
+
connections.page && connections.page.close(),
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
58
133
|
|
|
59
|
-
|
|
60
|
-
result.dynamicContentHTMLs[filePath] = html;
|
|
134
|
+
await browser.close();
|
|
61
135
|
|
|
62
|
-
|
|
136
|
+
TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
|
|
63
137
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
console.log('#', kleur.yellow(`WARNING: Static html file with no {{content}} detected. Therefore ignoring ${filePath}`));
|
|
67
|
-
result.staticHTMLs[filePath] = html;
|
|
138
|
+
if (config.after) {
|
|
139
|
+
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, 'after');
|
|
68
140
|
}
|
|
69
141
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
142
|
+
process.exit(config.COUNTER.failCount > 0 || hasFatalError ? 1 : 0);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
73
145
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
146
|
+
async function buildCachedContent(config, htmlPaths) {
|
|
147
|
+
const htmlBuffers = await Promise.all(config.htmlPaths.map((htmlPath) => fs.readFile(htmlPath)));
|
|
148
|
+
const cachedContent = htmlPaths.reduce(
|
|
149
|
+
(result, htmlPath, index) => {
|
|
150
|
+
const filePath = config.htmlPaths[index];
|
|
151
|
+
const html = htmlBuffers[index].toString();
|
|
152
|
+
|
|
153
|
+
if (html.includes('{{content}}')) {
|
|
154
|
+
result.dynamicContentHTMLs[filePath] = html;
|
|
155
|
+
result.htmlPathsToRunTests.push(filePath.replace(config.projectRoot, ''));
|
|
156
|
+
} else {
|
|
157
|
+
console.log(
|
|
158
|
+
'#',
|
|
159
|
+
kleur.yellow(
|
|
160
|
+
`WARNING: Static html file with no {{content}} detected. Therefore ignoring ${filePath}`,
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
result.staticHTMLs[filePath] = html;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
findInternalAssetsFromHTML(html).forEach((key) => {
|
|
167
|
+
result.assets.add(normalizeInternalAssetPathFromHTML(config.projectRoot, key, filePath));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
allTestCode: null,
|
|
174
|
+
assets: new Set(),
|
|
175
|
+
htmlPathsToRunTests: [],
|
|
176
|
+
mainHTML: { filePath: null, html: null },
|
|
177
|
+
staticHTMLs: {},
|
|
178
|
+
dynamicContentHTMLs: {},
|
|
179
|
+
},
|
|
180
|
+
);
|
|
83
181
|
|
|
84
182
|
if (cachedContent.htmlPathsToRunTests.length === 0) {
|
|
85
183
|
cachedContent.htmlPathsToRunTests = ['/'];
|
|
86
184
|
}
|
|
87
185
|
|
|
88
|
-
return
|
|
186
|
+
return addCachedContentMainHTML(config.projectRoot, cachedContent);
|
|
89
187
|
}
|
|
90
188
|
|
|
91
189
|
async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
92
|
-
|
|
190
|
+
const mainHTMLPath = Object.keys(cachedContent.dynamicContentHTMLs)[0];
|
|
93
191
|
if (mainHTMLPath) {
|
|
94
192
|
cachedContent.mainHTML = {
|
|
95
193
|
filePath: mainHTMLPath,
|
|
96
|
-
html: cachedContent.dynamicContentHTMLs[mainHTMLPath]
|
|
194
|
+
html: cachedContent.dynamicContentHTMLs[mainHTMLPath],
|
|
97
195
|
};
|
|
98
196
|
} else {
|
|
99
|
-
|
|
100
|
-
|
|
197
|
+
const html = (await fs.readFile(`${__dirname}/../boilerplates/setup/tests.hbs`)).toString();
|
|
101
198
|
cachedContent.mainHTML = { filePath: `${projectRoot}/test/tests.html`, html };
|
|
102
199
|
cachedContent.assets.add(`${projectRoot}/node_modules/qunitx/vendor/qunit.css`);
|
|
103
200
|
}
|
|
@@ -105,14 +202,27 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
105
202
|
return cachedContent;
|
|
106
203
|
}
|
|
107
204
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
205
|
+
function splitIntoGroups(files, groupCount) {
|
|
206
|
+
const groups = Array.from({ length: groupCount }, () => []);
|
|
207
|
+
files.forEach((file, i) => groups[i % groupCount].push(file));
|
|
208
|
+
return groups.filter((g) => g.length > 0);
|
|
111
209
|
}
|
|
112
210
|
|
|
113
|
-
function
|
|
114
|
-
|
|
211
|
+
function logWatcherAndKeyboardShortcutInfo(config, server) {
|
|
212
|
+
console.log(
|
|
213
|
+
'#',
|
|
214
|
+
kleur.blue(`Watching files... You can browse the tests on http://localhost:${config.port} ...`),
|
|
215
|
+
);
|
|
216
|
+
console.log(
|
|
217
|
+
'#',
|
|
218
|
+
kleur.blue(
|
|
219
|
+
`Shortcuts: Press "qq" to abort running tests, "qa" to run all the tests, "qf" to run last failing test, "ql" to repeat last test`,
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
115
223
|
|
|
224
|
+
function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
|
|
225
|
+
const currentDirectory = htmlPath ? htmlPath.split('/').slice(0, -1).join('/') : projectRoot;
|
|
116
226
|
return assetPath.startsWith('./')
|
|
117
227
|
? normalize(`${currentDirectory}/${assetPath.slice(2)}`)
|
|
118
228
|
: normalize(`${currentDirectory}/${assetPath}`);
|
package/lib/servers/http.js
CHANGED
|
@@ -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:
|
|
7
|
-
js:
|
|
8
|
-
css:
|
|
9
|
-
png:
|
|
10
|
-
jpg:
|
|
11
|
-
gif:
|
|
12
|
-
ico:
|
|
13
|
-
svg:
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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,16 +127,20 @@ 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
|
|
|
128
134
|
handleRequest(req, res) {
|
|
129
135
|
const { method, url } = req;
|
|
130
|
-
const
|
|
136
|
+
const urlObj = new URL(url, 'http://localhost');
|
|
137
|
+
const pathname = urlObj.pathname;
|
|
138
|
+
req.path = pathname;
|
|
139
|
+
req.query = Object.fromEntries(urlObj.searchParams);
|
|
140
|
+
const matchingRoute = this.findRouteHandler(method, pathname);
|
|
131
141
|
|
|
132
142
|
if (matchingRoute) {
|
|
133
|
-
req.params = this.extractParams(matchingRoute,
|
|
143
|
+
req.params = this.extractParams(matchingRoute, pathname);
|
|
134
144
|
this.runMiddleware(req, res, matchingRoute.handler);
|
|
135
145
|
} else {
|
|
136
146
|
res.statusCode = 404;
|
|
@@ -159,27 +169,32 @@ export default class HTTPServer {
|
|
|
159
169
|
return null;
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
return
|
|
163
|
-
|
|
172
|
+
return (
|
|
173
|
+
routes[url] ||
|
|
174
|
+
Object.values(routes).find((route) => {
|
|
175
|
+
const { path, isWildcard } = route;
|
|
164
176
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
if (!isWildcard && !path.includes(':')) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
168
180
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
if (isWildcard || this.matchPathSegments(path, url)) {
|
|
182
|
+
if (route.paramNames.length > 0) {
|
|
183
|
+
const regexPattern = this.buildRegexPattern(path, route.paramNames);
|
|
184
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
185
|
+
const regexMatches = regex.exec(url);
|
|
186
|
+
if (regexMatches) {
|
|
187
|
+
route.paramValues = regexMatches.slice(1);
|
|
188
|
+
}
|
|
176
189
|
}
|
|
190
|
+
return true;
|
|
177
191
|
}
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
192
|
|
|
181
|
-
|
|
182
|
-
|
|
193
|
+
return false;
|
|
194
|
+
}) ||
|
|
195
|
+
routes['/*'] ||
|
|
196
|
+
null
|
|
197
|
+
);
|
|
183
198
|
}
|
|
184
199
|
|
|
185
200
|
matchPathSegments(path, url) {
|
|
@@ -217,7 +232,7 @@ export default class HTTPServer {
|
|
|
217
232
|
const paramRegex = /:(\w+)/g;
|
|
218
233
|
const paramMatches = path.match(paramRegex);
|
|
219
234
|
|
|
220
|
-
return paramMatches ? paramMatches.map(match => match.slice(1)) : [];
|
|
235
|
+
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
221
236
|
}
|
|
222
237
|
|
|
223
238
|
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
|
-
|
|
6
|
-
|
|
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
|
}
|
package/lib/setup/browser.js
CHANGED
|
@@ -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(
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
// }
|
package/lib/setup/config.js
CHANGED
|
@@ -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
|
|
11
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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))
|
|
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
|
|