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.
- package/CHANGELOG.md +7 -0
- package/Dockerfile +24 -0
- package/LICENSE +22 -0
- package/build.js +54 -1
- package/cli.js +24 -1
- package/lib/boilerplates/default-project-config-values.js +6 -0
- package/lib/boilerplates/setup/tests.hbs +15 -0
- package/lib/boilerplates/setup/tsconfig.json +109 -0
- package/lib/boilerplates/test.js +25 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/help.js +38 -0
- package/lib/commands/init.js +70 -0
- package/lib/commands/run/tests-in-browser.js +162 -0
- package/lib/commands/run.js +119 -0
- package/lib/servers/http.js +233 -0
- package/lib/setup/bind-server-to-port.js +14 -0
- package/lib/setup/browser.js +55 -0
- package/lib/setup/config.js +46 -0
- package/lib/setup/file-watcher.js +72 -0
- package/lib/setup/fs-tree.js +48 -0
- package/lib/setup/keyboard-events.js +34 -0
- package/lib/setup/test-file-paths.js +79 -0
- package/lib/setup/web-server.js +241 -0
- package/lib/setup/write-output-static-files.js +22 -0
- package/lib/tap/display-final-result.js +15 -0
- package/lib/tap/display-test-result.js +73 -0
- package/lib/utils/find-internal-assets-from-html.js +16 -0
- package/lib/utils/find-project-root.js +17 -0
- package/lib/utils/indent-string.js +11 -0
- package/lib/utils/listen-to-keyboard-key.js +44 -0
- package/lib/utils/parse-cli-flags.js +57 -0
- package/lib/utils/path-exists.js +11 -0
- package/lib/utils/resolve-port-number-for.js +27 -0
- package/lib/utils/run-user-module.js +18 -0
- package/lib/utils/search-in-parent-directories.js +15 -0
- package/lib/utils/time-counter.js +8 -0
- package/package.json +1 -1
- package/test/commands/help-test.js +73 -0
- package/test/commands/index.js +2 -0
- package/test/commands/init-test.js +44 -0
- package/test/flags/after-test.js +23 -0
- package/test/flags/before-test.js +23 -0
- package/test/flags/coverage-test.js +6 -0
- package/test/flags/failfast-test.js +5 -0
- package/test/flags/index.js +2 -0
- package/test/flags/output-test.js +6 -0
- package/test/flags/reporter-test.js +6 -0
- package/test/flags/timeout-test.js +6 -0
- package/test/flags/watch-test.js +6 -0
- package/test/helpers/after-script-async.js +13 -0
- package/test/helpers/after-script-basic.js +1 -0
- package/test/helpers/assert-stdout.js +112 -0
- package/test/helpers/before-script-async.js +35 -0
- package/test/helpers/before-script-basic.js +1 -0
- package/test/helpers/before-script-web-server-tests.js +28 -0
- package/test/helpers/failing-tests.js +49 -0
- package/test/helpers/failing-tests.ts +49 -0
- package/test/helpers/fs-writers.js +36 -0
- package/test/helpers/index-with-content.html +20 -0
- package/test/helpers/index-without-content.html +22 -0
- package/test/helpers/passing-tests-dist.js +4883 -0
- package/test/helpers/passing-tests.js +44 -0
- package/test/helpers/passing-tests.ts +44 -0
- package/test/helpers/shell.js +37 -0
- package/test/index.js +22 -0
- package/test/inputs/advanced-htmls-test.js +21 -0
- package/test/inputs/error-edge-cases-test.js +11 -0
- package/test/inputs/file-and-folder-test.js +11 -0
- package/test/inputs/file-test.js +169 -0
- package/test/inputs/folder-test.js +193 -0
- package/test/inputs/index.js +5 -0
- package/test/setup/index.js +1 -0
- package/test/setup/test-file-paths-test.js +33 -0
- package/test/setup.js +17 -0
- package/vendor/package.json +1 -0
- package/vendor/qunit.css +525 -0
- package/vendor/qunit.js +7037 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.js';
|
|
4
|
+
import TAPDisplayTestResult from '../tap/display-test-result.js';
|
|
5
|
+
import pathExists from '../utils/path-exists.js';
|
|
6
|
+
import HTTPServer, { MIME_TYPES } from '../servers/http.js';
|
|
7
|
+
|
|
8
|
+
const fsPromise = fs.promises;
|
|
9
|
+
|
|
10
|
+
export default async function setupWebServer(config = {
|
|
11
|
+
port: 1234, debug: false, watch: false, timeout: 10000
|
|
12
|
+
}, cachedContent) {
|
|
13
|
+
let STATIC_FILES_PATH = path.join(config.projectRoot, config.output);
|
|
14
|
+
let server = new HTTPServer();
|
|
15
|
+
|
|
16
|
+
server.wss.on('connection', function connection(socket) {
|
|
17
|
+
socket.on('message', function message(data) {
|
|
18
|
+
const { event, details, abort } = JSON.parse(data);
|
|
19
|
+
|
|
20
|
+
if (event === "connection") {
|
|
21
|
+
console.log('TAP version 13');
|
|
22
|
+
} else if ((event === 'testEnd') && !abort) {
|
|
23
|
+
if (details.status === 'failed') {
|
|
24
|
+
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
TAPDisplayTestResult(config.COUNTER, details)
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
server.get('/', async (req, res) => {
|
|
33
|
+
let TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
34
|
+
let htmlContent = escapeAndInjectTestsToHTML(
|
|
35
|
+
replaceAssetPaths(cachedContent.mainHTML.html, cachedContent.mainHTML.filePath, config.projectRoot),
|
|
36
|
+
TEST_RUNTIME_TO_INJECT,
|
|
37
|
+
cachedContent.allTestCode
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
res.write(htmlContent);
|
|
41
|
+
res.end();
|
|
42
|
+
|
|
43
|
+
return await fsPromise.writeFile(`${config.projectRoot}/${config.output}/index.html`, htmlContent);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
server.get('/qunitx.html', async (req, res) => {
|
|
47
|
+
let TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
48
|
+
let htmlContent = escapeAndInjectTestsToHTML(
|
|
49
|
+
replaceAssetPaths(cachedContent.mainHTML.html, cachedContent.mainHTML.filePath, config.projectRoot),
|
|
50
|
+
TEST_RUNTIME_TO_INJECT,
|
|
51
|
+
cachedContent.filteredTestCode
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
res.write(htmlContent);
|
|
55
|
+
res.end();
|
|
56
|
+
|
|
57
|
+
return await fsPromise.writeFile(`${config.projectRoot}/${config.output}/qunitx.html`, htmlContent);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
server.get('/*', async (req, res) => {
|
|
61
|
+
let possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
|
|
62
|
+
if (possibleDynamicHTML) {
|
|
63
|
+
let TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
|
|
64
|
+
let htmlContent = escapeAndInjectTestsToHTML(
|
|
65
|
+
possibleDynamicHTML,
|
|
66
|
+
TEST_RUNTIME_TO_INJECT,
|
|
67
|
+
cachedContent.allTestCode
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
res.write(htmlContent);
|
|
71
|
+
res.end();
|
|
72
|
+
|
|
73
|
+
return await fsPromise.writeFile(`${config.projectRoot}/${config.output}${req.path}`, htmlContent);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let url = req.url;
|
|
77
|
+
let requestStartedAt = new Date();
|
|
78
|
+
let filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join('');
|
|
79
|
+
let statusCode = await pathExists(filePath) ? 200 : 404;
|
|
80
|
+
|
|
81
|
+
res.writeHead(statusCode, {
|
|
82
|
+
"Content-Type": req.headers.accept?.includes('text/html')
|
|
83
|
+
? MIME_TYPES.html
|
|
84
|
+
: MIME_TYPES[path.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (statusCode === 404) {
|
|
88
|
+
res.end();
|
|
89
|
+
} else {
|
|
90
|
+
fs.createReadStream(filePath)
|
|
91
|
+
.pipe(res);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(`# [HTTPServer] GET ${url} ${statusCode} - ${new Date() - requestStartedAt}ms`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return server;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
101
|
+
let assetPaths = findInternalAssetsFromHTML(html);
|
|
102
|
+
let htmlDirectory = htmlPath.split('/').slice(0, -1).join('/')
|
|
103
|
+
|
|
104
|
+
return assetPaths.reduce((result, assetPath) => {
|
|
105
|
+
let normalizedFullAbsolutePath = path.normalize(`${htmlDirectory}/${assetPath}`);
|
|
106
|
+
|
|
107
|
+
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, '.'));
|
|
108
|
+
}, html);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function testRuntimeToInject(port, config) {
|
|
112
|
+
return `<script>
|
|
113
|
+
window.testTimeout = 0;
|
|
114
|
+
setInterval(() => {
|
|
115
|
+
window.testTimeout = window.testTimeout + 1000;
|
|
116
|
+
}, 1000);
|
|
117
|
+
|
|
118
|
+
(function() {
|
|
119
|
+
function setupWebsocket() {
|
|
120
|
+
window.socket = new WebSocket('ws://localhost:${port}');
|
|
121
|
+
window.socket.addEventListener('message', function(messageEvent) {
|
|
122
|
+
if (!window.IS_PUPPETEER && messageEvent.data === 'refresh') {
|
|
123
|
+
window.location.reload(true);
|
|
124
|
+
} else if (window.IS_PUPPETEER && messageEvent.data === 'abort') {
|
|
125
|
+
window.abortQUnit = true;
|
|
126
|
+
window.QUnit.config.queue.length = 0;
|
|
127
|
+
window.socket.send(JSON.stringify({ event: 'abort' }))
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function trySetupWebsocket() {
|
|
133
|
+
try {
|
|
134
|
+
setupWebsocket();
|
|
135
|
+
} catch(error) {
|
|
136
|
+
console.log(error);
|
|
137
|
+
window.setTimeout(() => trySetupWebsocket(), 10);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
trySetupWebsocket();
|
|
142
|
+
})();
|
|
143
|
+
|
|
144
|
+
{{allTestCode}}
|
|
145
|
+
|
|
146
|
+
function getCircularReplacer() {
|
|
147
|
+
const ancestors = [];
|
|
148
|
+
return function (key, value) {
|
|
149
|
+
if (typeof value !== "object" || value === null) {
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
153
|
+
ancestors.pop();
|
|
154
|
+
}
|
|
155
|
+
if (ancestors.includes(value)) {
|
|
156
|
+
return "[Circular]";
|
|
157
|
+
}
|
|
158
|
+
ancestors.push(value);
|
|
159
|
+
return value;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function setupQUnit() {
|
|
164
|
+
window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, currentTest: '' };
|
|
165
|
+
|
|
166
|
+
if (!window.socket ) {
|
|
167
|
+
return window.setTimeout(() => setupQUnit(), 10);
|
|
168
|
+
} else if (!window.QUnit || !window.QUnit.moduleStart || window.QUnit.config.queue === 0) {
|
|
169
|
+
if (socket.readyState == 0) {
|
|
170
|
+
return window.setTimeout(() => setupQUnit(), 10);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
window.testTimeout = ${config.timeout};
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
|
|
178
|
+
if (window.IS_PUPPETEER) {
|
|
179
|
+
window.socket.send(JSON.stringify({ event: 'connection' }));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
window.QUnit.moduleStart((details) => { // NOTE: might be useful in future for hanged module tracking
|
|
183
|
+
if (window.IS_PUPPETEER) {
|
|
184
|
+
window.socket.send(JSON.stringify({ event: 'moduleStart', details: details }, getCircularReplacer()));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
window.QUnit.on('testStart', (details) => {
|
|
188
|
+
window.QUNIT_RESULT.totalTests++;
|
|
189
|
+
window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
|
|
190
|
+
});
|
|
191
|
+
window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
192
|
+
window.testTimeout = 0;
|
|
193
|
+
window.QUNIT_RESULT.finishedTests++;
|
|
194
|
+
window.QUNIT_RESULT.currentTest = null;
|
|
195
|
+
if (window.IS_PUPPETEER) {
|
|
196
|
+
window.socket.send(JSON.stringify({ event: 'testEnd', details: details, abort: window.abortQUnit }, getCircularReplacer()));
|
|
197
|
+
|
|
198
|
+
if (${config.failFast} && details.status === 'failed') {
|
|
199
|
+
window.QUnit.config.queue.length = 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
window.QUnit.done((details) => {
|
|
204
|
+
if (window.IS_PUPPETEER) {
|
|
205
|
+
window.setTimeout(() => {
|
|
206
|
+
window.socket.send(JSON.stringify({ event: 'done', details: details, abort: window.abortQUnit }, getCircularReplacer()))
|
|
207
|
+
}, 50);
|
|
208
|
+
}
|
|
209
|
+
window.setTimeout(() => {
|
|
210
|
+
window.testTimeout = ${config.timeout};
|
|
211
|
+
}, 75);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if ([1, 3].includes(window.socket.readyState)) {
|
|
215
|
+
return window.setTimeout(() => window.QUnit.start(), 25);
|
|
216
|
+
} else {
|
|
217
|
+
let connectionTrialCount = 0;
|
|
218
|
+
let connectionInterval = window.setInterval(() => {
|
|
219
|
+
if ([1, 3].includes(window.socket.readyState) || connectionTrialCount > 25) {
|
|
220
|
+
window.clearInterval(connectionInterval);
|
|
221
|
+
|
|
222
|
+
return window.setTimeout(() => window.QUnit.start(), 25);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
connectionTrialCount = connectionTrialCount + 1;
|
|
226
|
+
}, 10);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
setupQUnit();
|
|
231
|
+
</script>`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function escapeAndInjectTestsToHTML(html, testRuntimeCode, testContentCode) {
|
|
235
|
+
return html
|
|
236
|
+
.replace('{{content}}',
|
|
237
|
+
testRuntimeCode
|
|
238
|
+
.replace('{{allTestCode}}', testContentCode)
|
|
239
|
+
.replace('</script>', '<\/script>') // NOTE: remove this when simple-html-tokenizer PR gets merged
|
|
240
|
+
);
|
|
241
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export default async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
|
|
4
|
+
let staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
5
|
+
let htmlRelativePath = staticHTMLKey.replace(`${projectRoot}/`, '');
|
|
6
|
+
|
|
7
|
+
await ensureFolderExists(`${projectRoot}/${output}/${htmlRelativePath}`);
|
|
8
|
+
await fs.writeFile(`${projectRoot}/${output}/${htmlRelativePath}`, cachedContent.staticHTMLs[staticHTMLKey]);
|
|
9
|
+
});
|
|
10
|
+
let assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
|
|
11
|
+
let assetRelativePath = assetAbsolutePath.replace(`${projectRoot}/`, '');
|
|
12
|
+
|
|
13
|
+
await ensureFolderExists(`${projectRoot}/${output}/${assetRelativePath}`);
|
|
14
|
+
await fs.copyFile(assetAbsolutePath, `${projectRoot}/${output}/${assetRelativePath}`);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function ensureFolderExists(assetPath) {
|
|
21
|
+
await fs.mkdir(assetPath.split('/').slice(0, -1).join('/'), { recursive: true });
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default function({ testCount, passCount, skipCount, failCount }, timeTaken) {
|
|
2
|
+
console.log('');
|
|
3
|
+
console.log(`1..${testCount}`);
|
|
4
|
+
console.log(`# tests ${testCount}`);
|
|
5
|
+
console.log(`# pass ${passCount}`);
|
|
6
|
+
console.log(`# skip ${skipCount}`);
|
|
7
|
+
console.log(`# fail ${failCount}`);
|
|
8
|
+
|
|
9
|
+
// let seconds = timeTaken > 1000 ? Math.floor(timeTaken / 1000) : 0;
|
|
10
|
+
// let milliseconds = timeTaken % 100;
|
|
11
|
+
|
|
12
|
+
console.log(`# duration ${timeTaken}`);
|
|
13
|
+
console.log('');
|
|
14
|
+
}
|
|
15
|
+
// console.log(details.timeTaken); // runtime
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import yaml from 'js-yaml'
|
|
2
|
+
import indentString from '../utils/indent-string.js';
|
|
3
|
+
|
|
4
|
+
// tape TAP output: ['operator', 'stack', 'at', 'expected', 'actual']
|
|
5
|
+
// ava TAP output: ['message', 'name', 'at', 'assertion', 'values'] // Assertion #5, message
|
|
6
|
+
export default function(COUNTER, details) { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
|
|
7
|
+
COUNTER.testCount++;
|
|
8
|
+
|
|
9
|
+
if (details.status === 'skipped') {
|
|
10
|
+
COUNTER.skipCount++;
|
|
11
|
+
console.log(`ok ${COUNTER.testCount}`, details.fullName.join(' | '), '# skip');
|
|
12
|
+
} else if (details.status === 'todo') {
|
|
13
|
+
console.log(`not ok ${COUNTER.testCount}`, details.fullName.join(' | '), '# skip');
|
|
14
|
+
} else if (details.status === 'failed') {
|
|
15
|
+
COUNTER.failCount++;
|
|
16
|
+
console.log(`not ok ${COUNTER.testCount}`, details.fullName.join(' | '), `# (${details.runtime.toFixed(0)} ms)`);
|
|
17
|
+
details.assertions.reduce((errorCount, assertion, index) => {
|
|
18
|
+
if (!assertion.passed && assertion.todo === false) {
|
|
19
|
+
COUNTER.errorCount++;
|
|
20
|
+
let stack = assertion.stack?.match(/\(.+\)/g);
|
|
21
|
+
|
|
22
|
+
console.log(' ---');
|
|
23
|
+
console.log(indentString(yaml.dump({
|
|
24
|
+
name: `Assertion #${index + 1}`, // TODO: check what happens on runtime errors
|
|
25
|
+
actual: assertion.actual ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
|
|
26
|
+
expected: assertion.expected ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
|
|
27
|
+
message: assertion.message || null,
|
|
28
|
+
stack: assertion.stack || null,
|
|
29
|
+
at: stack ? stack[0].replace('(file://', '').replace(')', '') : null
|
|
30
|
+
}), 4));
|
|
31
|
+
console.log(' ...');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return errorCount;
|
|
35
|
+
}, 0);
|
|
36
|
+
} else if (details.status === 'passed') {
|
|
37
|
+
COUNTER.passCount++;
|
|
38
|
+
console.log(`ok ${COUNTER.testCount}`, details.fullName.join(' | '), `# (${details.runtime.toFixed(0)} ms)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getCircularReplacer() {
|
|
43
|
+
const ancestors = [];
|
|
44
|
+
return function (key, value) {
|
|
45
|
+
if (typeof value !== "object" || value === null) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
|
|
49
|
+
ancestors.pop();
|
|
50
|
+
}
|
|
51
|
+
if (ancestors.includes(value)) {
|
|
52
|
+
return "[Circular]";
|
|
53
|
+
}
|
|
54
|
+
ancestors.push(value);
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// not ok 10 test exited without ending: deepEqual true works
|
|
60
|
+
// ---
|
|
61
|
+
// operator: fail
|
|
62
|
+
// at: process.<anonymous> (/home/izelnakri/ava-test/node_modules/tape/index.js:85:19)
|
|
63
|
+
// stack: |-
|
|
64
|
+
// Error: test exited without ending: deepEqual true works
|
|
65
|
+
// at Test.assert [as _assert] (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:269:54)
|
|
66
|
+
// at Test.bound [as _assert] (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:90:32)
|
|
67
|
+
// at Test.fail (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:363:10)
|
|
68
|
+
// at Test.bound [as fail] (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:90:32)
|
|
69
|
+
// at Test._exit (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:226:14)
|
|
70
|
+
// at Test.bound [as _exit] (/home/izelnakri/ava-test/node_modules/tape/lib/test.js:90:32)
|
|
71
|
+
// at process.<anonymous> (/home/izelnakri/ava-test/node_modules/tape/index.js:85:19)
|
|
72
|
+
// at process.emit (node:events:376:20)
|
|
73
|
+
// ...
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import cheerio from 'cheerio';
|
|
2
|
+
|
|
3
|
+
const ABSOLUTE_URL_REGEX = new RegExp('^(?:[a-z]+:)?//', 'i');
|
|
4
|
+
|
|
5
|
+
export default function findInternalAssetsFromHTML(htmlContent) {
|
|
6
|
+
const $ = cheerio.load(htmlContent);
|
|
7
|
+
const internalJSFiles = $('script[src]').toArray()
|
|
8
|
+
.map((scriptNode) => $(scriptNode).attr('src'))
|
|
9
|
+
.filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
10
|
+
const internalCSSFiles = $('link[href]').toArray()
|
|
11
|
+
.map((scriptNode) => $(scriptNode).attr('href'))
|
|
12
|
+
.filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
13
|
+
|
|
14
|
+
return internalCSSFiles.concat(internalJSFiles);
|
|
15
|
+
// TODO: maybe needs normalization ? .map((fileReferencePath) => fileReferencePath.replace('/assets', `${projectRoot}/tmp/assets`));
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import searchInParentDirectories from './search-in-parent-directories.js';
|
|
3
|
+
|
|
4
|
+
export default async function() {
|
|
5
|
+
try {
|
|
6
|
+
let absolutePath = await searchInParentDirectories('.', 'package.json');
|
|
7
|
+
if (!absolutePath.includes('package.json')) {
|
|
8
|
+
throw new Error('package.json mising');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return absolutePath.replace('/package.json', '');
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.log('couldnt find projects package.json, did you run $ npm init ??');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default function indentString(string, count = 1, options = {}) {
|
|
2
|
+
const { indent = ' ', includeEmptyLines = false } = options;
|
|
3
|
+
|
|
4
|
+
if (count <= 0) {
|
|
5
|
+
return string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const regex = includeEmptyLines ? /^/gm : /^(?!\s*$)/gm;
|
|
9
|
+
|
|
10
|
+
return string.replace(regex, indent.repeat(count));
|
|
11
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
|
|
3
|
+
let stdin = process.stdin;
|
|
4
|
+
let targetInputs = {};
|
|
5
|
+
let inputs = [];
|
|
6
|
+
let listenerAdded = false;
|
|
7
|
+
|
|
8
|
+
export default function listenToKeyboardKey(inputString, closure, options = { caseSensitive: false }) {
|
|
9
|
+
stdin.setRawMode(true);
|
|
10
|
+
stdin.resume();
|
|
11
|
+
stdin.setEncoding('utf8');
|
|
12
|
+
if (!listenerAdded) {
|
|
13
|
+
stdin.on('data', function(key){
|
|
14
|
+
if (key === '\u0003') {
|
|
15
|
+
process.exit(); // so node process doesnt trap Control-C
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
inputs.shift();
|
|
19
|
+
inputs.push(key);
|
|
20
|
+
|
|
21
|
+
let inputString = inputs.join('');
|
|
22
|
+
let targetListener = targetInputs[inputString.toUpperCase()];
|
|
23
|
+
if (targetListener && targetListenerConformsToCase(targetListener, inputString)) {
|
|
24
|
+
targetListener.closure(inputString);
|
|
25
|
+
inputs.fill(undefined);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
listenerAdded = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (inputString.length > inputs.length) {
|
|
32
|
+
inputs.length = inputString.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
targetInputs[inputString.toUpperCase()] = Object.assign(options, { closure });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function targetListenerConformsToCase(targetListener, inputString) {
|
|
39
|
+
if (targetListener.caseSensitive) {
|
|
40
|
+
return inputString === inputString.toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// { inputs: [], debug: true, watch: true, failFast: true, htmlPaths: [], output }
|
|
2
|
+
export default async function(projectRoot) {
|
|
3
|
+
const providedFlags = process.argv.slice(2).reduce((result, arg) => {
|
|
4
|
+
if (arg.startsWith('--debug')) {
|
|
5
|
+
return Object.assign(result, { debug: parseBoolean(arg.split('=')[1]) });
|
|
6
|
+
} else if (arg.startsWith('--watch')) {
|
|
7
|
+
return Object.assign(result, { watch: parseBoolean(arg.split('=')[1]) });
|
|
8
|
+
} else if (arg.startsWith('--failfast') || arg.startsWith('--failFast')) {
|
|
9
|
+
return Object.assign(result, { failFast: parseBoolean(arg.split('=')[1]) });
|
|
10
|
+
} else if (arg.startsWith('--timeout')) {
|
|
11
|
+
return Object.assign(result, { timeout: arg.split('=')[1] || 10000 });
|
|
12
|
+
} else if (arg.startsWith('--output')) {
|
|
13
|
+
return Object.assign(result, { output: arg.split('=')[1] });
|
|
14
|
+
} else if (arg.endsWith('.html')) {
|
|
15
|
+
if (result.htmlPaths) {
|
|
16
|
+
result.htmlPaths.push(arg);
|
|
17
|
+
} else {
|
|
18
|
+
result.htmlPaths = [arg];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result;
|
|
22
|
+
} else if (arg.startsWith('--port')) {
|
|
23
|
+
return Object.assign(result, { port: Number(arg.split('=')[1]) });
|
|
24
|
+
} else if (arg.startsWith('--before')) {
|
|
25
|
+
return Object.assign(result, { before: parseModule(arg.split('=')[1]) });
|
|
26
|
+
} else if (arg.startsWith('--after')) {
|
|
27
|
+
return Object.assign(result, { after: parseModule(arg.split('=')[1]) });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// maybe set watch depth via micromatch(so incl metadata)
|
|
31
|
+
result.inputs.add(arg.startsWith(projectRoot) ? arg : `${process.cwd()}/${arg}`);
|
|
32
|
+
|
|
33
|
+
return result;
|
|
34
|
+
}, { inputs: new Set([]) });
|
|
35
|
+
|
|
36
|
+
providedFlags.inputs = Array.from(providedFlags.inputs);
|
|
37
|
+
|
|
38
|
+
return providedFlags;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseBoolean(result, defaultValue=true) {
|
|
42
|
+
if (result === 'true') {
|
|
43
|
+
return true;
|
|
44
|
+
} else if (result === 'false') {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return defaultValue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseModule(value) {
|
|
52
|
+
if (['false', "'false'", '"false"', ''].includes(value)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default async function resolvePortNumberFor(portNumber) {
|
|
2
|
+
if (await portIsAvailable(portNumber)) {
|
|
3
|
+
return portNumber;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return (await resolvePortNumberFor(portNumber + 1));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function portIsAvailable(portNumber) {
|
|
10
|
+
return new Promise(async (resolve) => {
|
|
11
|
+
const net = await import('net');
|
|
12
|
+
const server = net.createServer();
|
|
13
|
+
|
|
14
|
+
server.once('error', function(err) {
|
|
15
|
+
if (err.code === 'EADDRINUSE') {
|
|
16
|
+
resolve(false);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
server.once('listening', function() {
|
|
21
|
+
server.close();
|
|
22
|
+
resolve(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
server.listen(portNumber);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
|
|
3
|
+
export default async function runUserModule(modulePath, params, scriptPosition) {
|
|
4
|
+
try {
|
|
5
|
+
let func = await import(modulePath);
|
|
6
|
+
if (func) {
|
|
7
|
+
func.default ?
|
|
8
|
+
await func.default(params) :
|
|
9
|
+
typeof func === 'function' ? await func(params) : null;
|
|
10
|
+
}
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.log('#', kleur.red(`QUnitX ${scriptPosition} script failed:`));
|
|
13
|
+
console.trace(error);
|
|
14
|
+
console.error(error);
|
|
15
|
+
|
|
16
|
+
return process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import pathExists from './path-exists.js';
|
|
2
|
+
|
|
3
|
+
async function searchInParentDirectories(directory, targetEntry) {
|
|
4
|
+
directory = directory === '.' ? process.cwd() : directory;
|
|
5
|
+
|
|
6
|
+
if (await pathExists(`${directory}/${targetEntry}`)) {
|
|
7
|
+
return `${directory}/${targetEntry}`;
|
|
8
|
+
} else if (directory === '') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return await searchInParentDirectories(directory.slice(0, directory.lastIndexOf('/')), targetEntry);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default searchInParentDirectories;
|
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { module, test } from 'qunitx';
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { exec } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const CWD = process.cwd();
|
|
8
|
+
const VERSION = JSON.parse(fs.readFileSync(`${CWD}/package.json`)).version;
|
|
9
|
+
const shell = promisify(exec);
|
|
10
|
+
const cli = async function(arg = '') {
|
|
11
|
+
if (process.argv[0].includes('deno')) {
|
|
12
|
+
return await shell(`deno run --allow-read --allow-env ${CWD}/deno/cli.js ${arg}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return await shell(`node ${CWD}/cli.js ${arg}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const printedHelpOutput = `[qunitx v${VERSION}] Usage: qunitx [targets] --$flags
|
|
19
|
+
|
|
20
|
+
Input options:
|
|
21
|
+
- File: $ qunitx test/foo.js
|
|
22
|
+
- Folder: $ qunitx test/login
|
|
23
|
+
- Globs: $ qunitx test/**/*-test.js
|
|
24
|
+
- Combination: $ qunitx test/foo.js test/bar.js test/*-test.js test/logout
|
|
25
|
+
|
|
26
|
+
Optional flags:
|
|
27
|
+
--browser : run qunit tests in chromium with puppeteer instead of node.js(which is the default)
|
|
28
|
+
--debug : print console output when tests run in browser
|
|
29
|
+
--watch : run the target file or folders, watch them for continuous run and expose http server under localhost
|
|
30
|
+
--timeout : change default timeout per test case
|
|
31
|
+
--output : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
32
|
+
--failFast : run the target file or folders with immediate abort if a single test fails
|
|
33
|
+
--before : run a script before the tests(i.e start a new web server before tests)
|
|
34
|
+
--after : run a script after the tests(i.e save test results to a file)
|
|
35
|
+
|
|
36
|
+
Example: $ qunitx test/foo.ts app/e2e --browser --debug --watch --before=scripts/start-new-webserver.js --after=scripts/write-test-results.js
|
|
37
|
+
|
|
38
|
+
Commands:
|
|
39
|
+
$ qunitx init # Bootstraps qunitx base html and add qunitx config to package.json if needed
|
|
40
|
+
$ qunitx new $testFileName # Creates a qunitx test file`;
|
|
41
|
+
|
|
42
|
+
module('Commands | Help tests', () => {
|
|
43
|
+
test('$ qunitx -> prints help text', async (assert) => {
|
|
44
|
+
const { stdout } = await cli();
|
|
45
|
+
|
|
46
|
+
console.log(stdout);
|
|
47
|
+
assert.ok(stdout.includes(printedHelpOutput));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('$ qunitx print -> prints help text', async (assert) => {
|
|
51
|
+
const { stdout } = await cli('print');
|
|
52
|
+
|
|
53
|
+
assert.ok(stdout.includes(printedHelpOutput));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('$ qunitx p -> prints help text', async (assert) => {
|
|
57
|
+
const { stdout } = await cli('p');
|
|
58
|
+
|
|
59
|
+
assert.ok(stdout.includes(printedHelpOutput));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('$ qunitx help -> prints help text', async (assert) => {
|
|
63
|
+
const { stdout } = await cli('help');
|
|
64
|
+
|
|
65
|
+
assert.ok(stdout.includes(printedHelpOutput));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('$ qunitx h -> prints help text', async (assert) => {
|
|
69
|
+
const { stdout } = await cli('h');
|
|
70
|
+
|
|
71
|
+
assert.ok(stdout.includes(printedHelpOutput));
|
|
72
|
+
});
|
|
73
|
+
});
|