qunitx-cli 0.0.2 → 0.1.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.
- package/.github/dependabot.yml +7 -0
- package/.github/workflows/push.yml +36 -0
- package/CHANGELOG.md +16 -0
- package/Dockerfile +24 -0
- package/LICENSE +22 -0
- package/TODO +90 -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 +37 -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 +8 -5
- package/test/commands/help-test.js +72 -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,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
|
+
}
|
|
@@ -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
|
+
}
|