Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial --watch mode #2

Merged
merged 11 commits into from
Apr 22, 2021
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
tmp
vendor
6 changes: 6 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ dist
docs
tmp
.github
scripts
.gitignore
CHANGELOG.md
Dockerfile
README.md
TODO
4 changes: 4 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
- autorefresh: true
- QX keybind to run all tests
- regex needs to be there

$ qunitx some-test --browser | default html doesnt match with $ qunitx init and also no html mode should be there

try this: $ npx ava --tap | npx tap-difflet
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import QUnit from 'qunit';
import QUnit from './vendor/qunit.js';

QUnit.config.autostart = false;

Expand Down
2 changes: 1 addition & 1 deletion lib/commands/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ ${highlight("Input options:")}
${highlight("Optional flags:")}
${color('--browser')} : run qunit tests in chromium with puppeteer instead of node.js(which is the default)
${color('--debug')} : print console output when tests run in browser
${color('--watch')} : run the target file or folders and watch them for continuous run
${color('--watch')} : run the target file or folders, watch them for continuous run and expose http server under localhost
${color('--timeout')} : change default timeout per test case
${color('--output')} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
${color('--failFast')} : run the target file or folders with immediate abort if a single test fails
Expand Down
2 changes: 1 addition & 1 deletion lib/commands/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default async function() {
}

return result;
}, oldPackageJSON.qunitx ? oldPackageJSON.qunitx.htmlPaths : []);
}, oldPackageJSON.qunitx && oldPackageJSON.qunitx.htmlPaths ? oldPackageJSON.qunitx.htmlPaths : []);
let newQunitxConfig = Object.assign(
defaultProjectConfigValues,
htmlPaths.length > 0 ? { htmlPaths } : { htmlPaths: ['test/tests.html'] },
Expand Down
113 changes: 37 additions & 76 deletions lib/commands/run.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,28 @@
// let tree = Object.assign(tree, directoryReader(config, inputs, (file) => {
// import(`${file}`);
// }));
// TODO: make it run an in-memory html if not exists

import fs from 'fs/promises';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import kleur from 'kleur';
import esbuild from 'esbuild';
import setupWebsocketServer from '../setup/websocket-server.js';
import setupBrowser from '../setup/browser.js';
import parseFsInputs from '../utils/parse-fs-inputs.js';
import TAPDisplayFinalResult from '../tap/display-final-result.js';
import defaultProjectConfigValues from '../boilerplates/default-project-config-values.js';

import runTestsInNode from './run/tests-in-node.js';
import runTestsInBrowser from './run/tests-in-browser.js';
import fileWatcher from '../setup/file-watcher.js';
import setupNodeJSEnvironment from '../setup/node-js-environment.js';
import WebSocket from 'ws';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default async function(config) {
const { browser, fileOrFolderInputs, projectRoot, timeout, output } = config;
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 };

if (browser) {
let COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 };
const fsTree = await parseFsInputs(fileOrFolderInputs, {}, async(targetPath, fsEntry) => {
fsEntry.executed = false;
}, config);

await esbuild.build({
stdin: {
contents: Object.keys(fsTree).reduce((result, fileAbsolutePath) => {
return result + `import "${fileAbsolutePath}";`
}, ''),
resolveDir: process.cwd()
},
bundle: true,
logLevel: 'error',
outfile: `${projectRoot}/${output}/tests.js`
});
if (config.browser) {
// TODO: make static css/js middleware and output all needed files to config.outputFolder

let [QUnitCSS, allTestCode] = await Promise.all([
let [QUnitCSS, ...htmlBuffers] = await Promise.all([
fs.readFile(`${process.cwd()}/node_modules/qunit/qunit/qunit.css`),
fs.readFile(`${projectRoot}/${output}/tests.js`)
]);
let htmlBuffers = config.htmlPaths
await Promise.all(config.htmlPaths.map((htmlPath) => fs.readFile(`${projectRoot}/${htmlPath}`))); // TODO: remove this and read it from the fsTree

let codeInputs = config.htmlPaths.reduce((result, htmlPath, index) => {
].concat(config.htmlPaths.map((htmlPath) => fs.readFile(`${projectRoot}/${htmlPath}`)))); // TODO: remove this and read it from the fsTree, should be cached?
let cachedContent = config.htmlPaths.reduce((result, htmlPath, index) => {
let fileName = config.htmlPaths[index];
let html = htmlBuffers[index].toString();

if (!html.includes('{{content}}')) {
if (!html.includes('{{content}}')) { // TODO: here I could do html analysis to see which static js certain html points to? Complex algorithm
result.staticHTMLs[fileName] = html;
} else {
result.dynamicContentHTMLs[fileName] = html;
Expand All @@ -67,57 +40,45 @@ export default async function(config) {
return result;
}, {
QUnitCSS: QUnitCSS.toString(),
allTestCode: allTestCode.toString(),
allTestCode: null,
mainHTML: null,
staticHTMLs: {},
dynamicContentHTMLs: {}
});

if (!codeInputs.mainHTML || !codeInputs.mainHTML.html) {
codeInputs.mainHTML = {
if (!cachedContent.mainHTML || !cachedContent.mainHTML.html) {
cachedContent.mainHTML = {
fileName: '/',
html: (await fs.readFile(`${__dirname}/../boilerplates/setup/tests.hbs`)).toString()
}
}

// TODO: make these async end
let MBER_TEST_TIME_COUNTER = (function() {
const startTime = new Date();

return {
start: startTime,
stop: () => +(new Date()) - (+startTime)
};
})();
let { browser, server, WebSocketServer } = await setupBrowser(COUNTER, config, codeInputs);
const { server, page, WebSocketServer, browser } = await runTestsInBrowser(config.fileOrFolderInputs, config, cachedContent);

let TIME_TAKEN = MBER_TEST_TIME_COUNTER.stop()
if (config.watch) {
logWatcherAndKeyboardShortcutInfo();

TAPDisplayFinalResult(COUNTER, TIME_TAKEN);

// TODO: chokidar watch and run per test/file a test depends on? - big problem? run all?
if (!config.watch) {
await Promise.all([
server.close(),
browser.close(),
WebSocketServer.close()
]);

process.exit(COUNTER.failCount > 0 ? 1 : 0);
await fileWatcher(config.fileOrFolderInputs, async (file) => {
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 };
await runTestsInBrowser([file], config, cachedContent, { server, page, WebSocketServer, browser }) // TODO: check if its html then run html otherwise run all html, how??
}, () => {}, config);
}
} else {
global.testTimeout = timeout;
const QUnit = (await import('../setup/node-js-environment.js')).default;
const fsTree = await parseFsInputs(fileOrFolderInputs, {}, async(targetPath, fsEntry) => {
// try {
await import(targetPath);
// } catch(error) {
// }
fsEntry.executed = true;
}, config);
console.log('TAP version 13');
global.testTimeout = config.timeout;

setupNodeJSEnvironment(config);
await runTestsInNode(config.fileOrFolderInputs, config);

QUnit.start();
if (config.watch) {
logWatcherAndKeyboardShortcutInfo();

await fileWatcher(config.fileOrFolderInputs, async (file) => {
await runTestsInNode([file], config)
}, () => {}, config);
}
}
}

function logWatcherAndKeyboardShortcutInfo() {
console.log('# Watching files...'); // NOTE: maybe add also qx to exit
// console.log('# Watching files.. Press "qa" to run all the tests, "ql" to run last failing test'); // NOTE: maybe add also qx to exit
}
53 changes: 53 additions & 0 deletions lib/commands/run/tests-in-browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import fs from 'fs/promises';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import esbuild from 'esbuild';
import setupBrowser from '../../setup/browser.js';
import parseFsInputs from '../../utils/parse-fs-inputs.js';
import timeCounter from '../../utils/time-counter.js';
import TAPDisplayFinalResult from '../../tap/display-final-result.js';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default async function runTestsInBrowser(fileOrFolderInputs, config, cachedContent = {}, connections) {
const { projectRoot, timeout, output } = config;

const fsTree = await parseFsInputs(fileOrFolderInputs, {}, async(targetPath, fsEntry) => {
fsEntry.executed = false;
}, config);

await esbuild.build({
stdin: {
contents: Object.keys(fsTree).reduce((result, fileAbsolutePath) => {
return result + `import "${fileAbsolutePath}";`
}, ''),
resolveDir: process.cwd()
},
bundle: true,
logLevel: 'error',
outfile: `${projectRoot}/${output}/tests.js`
}); // NOTE: This prevents file cache most likely

cachedContent.allTestCode = await fs.readFile(`${projectRoot}/${output}/tests.js`);
// TODO: should I check something from config.htmlPaths at this point in the future?? NOTE: let htmlBuffers = await Promise.all(config.htmlPaths.map((htmlPath) => fs.readFile(`${projectRoot}/${htmlPath}`)));

let TIME_COUNTER = timeCounter();

let { browser, server, WebSocketServer } = await setupBrowser(config, cachedContent, connections);

let TIME_TAKEN = TIME_COUNTER.stop()

TAPDisplayFinalResult(config.COUNTER, TIME_TAKEN);

if (!config.watch) {
await Promise.all([
server.close(),
browser.close(),
WebSocketServer.close()
]);

return process.exit(config.COUNTER.failCount > 0 ? 1 : 0);
}

return { server, browser, WebSocketServer };
}
35 changes: 35 additions & 0 deletions lib/commands/run/tests-in-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import parseFsInputs from '../../utils/parse-fs-inputs.js';

let count = 0;

export default async function runTestsInNode(fileOrFolderInputs, config) {
config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 };

global.testTimeout = config.timeout;

let waitForTestCompletion = setupTestWaiting();

window.QUnit.reset();

const fsTree = await parseFsInputs(fileOrFolderInputs, {}, async (targetPath, fsEntry) => {
count = count + 1;

await import(`${targetPath}?${count}`);

fsEntry.executed = true;
}, config);

console.log('TAP version 13');
window.QUnit.start();

await waitForTestCompletion;
}

function setupTestWaiting() {
let finalizeTestCompletion;
let promise = new Promise((resolve, reject) => finalizeTestCompletion = resolve);

window.QUnit.done(() => finalizeTestCompletion());

return promise;
}
6 changes: 3 additions & 3 deletions lib/setup/bind-server-to-port.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import resolvePortNumberFor from '../utils/resolve-port-number-for.js';


// TODO: there is a race condition between socket.connection and server.listen
export default async function bindServerToPort(server, WebSocketServer, COUNTER, config, promise) {
export default async function bindServerToPort(server, WebSocketServer, config, promise) {
WebSocketServer.on('connection', (webSocket) => {
console.log('TAP version 13');

webSocket.on('message', (message) => {
const { event, details } = JSON.parse(message);

if (event === 'testEnd') {
TAPDisplayTestResult(COUNTER, details)
TAPDisplayTestResult(config.COUNTER, details)
}
});
});
Expand All @@ -28,7 +28,7 @@ export default async function bindServerToPort(server, WebSocketServer, COUNTER,

server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
bindServerToPort(server, WebSocketServer, COUNTER, Object.assign(config, { httpPort: config.httpPort + 1 }), promise);
bindServerToPort(server, WebSocketServer, Object.assign(config, { httpPort: config.httpPort + 1 }), promise);
}
console.log('UNKNOWN ERROR', e);
});
Expand Down
Loading