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,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,11 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ export default async function pathExists(path) {
4
+ try {
5
+ await fs.access(path);
6
+
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
@@ -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;
@@ -0,0 +1,8 @@
1
+ export default function() {
2
+ const startTime = new Date();
3
+
4
+ return {
5
+ startTime: startTime,
6
+ stop: () => +(new Date()) - (+startTime)
7
+ };
8
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "main": "index.js",
7
7
  "author": "Izel Nakri",
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ import "./help-test.js";
2
+ // import "./init-test.js";