Skip to content

Commit

Permalink
Merge pull request #2 from izelnakri/watcher
Browse files Browse the repository at this point in the history
initial --watch mode
  • Loading branch information
izelnakri committed Apr 22, 2021
2 parents 0453ee3 + 33375d6 commit ac7ab42
Show file tree
Hide file tree
Showing 21 changed files with 1,096 additions and 935 deletions.
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

0 comments on commit ac7ab42

Please sign in to comment.