From 198ab41031fa378c514e86d9b65a9e829d8c627e Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sun, 28 Mar 2021 15:28:50 +0200 Subject: [PATCH 01/11] better help command message for --watch --- lib/commands/help.js | 2 +- test/commands/help-test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commands/help.js b/lib/commands/help.js index 918d85c..55b2b91 100644 --- a/lib/commands/help.js +++ b/lib/commands/help.js @@ -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 diff --git a/test/commands/help-test.js b/test/commands/help-test.js index ff14cd0..e3e7bb1 100644 --- a/test/commands/help-test.js +++ b/test/commands/help-test.js @@ -15,7 +15,7 @@ Input options: Optional flags: --browser : run qunit tests in chromium with puppeteer instead of node.js(which is the default) --debug : print console output when tests run in browser ---watch : run the target file or folders and watch them for continuous run +--watch : run the target file or folders, watch them for continuous run and expose http server under localhost --timeout : change default timeout per test case --output : folder to distribute built qunitx html and js that a webservers can run[default: tmp] --failFast : run the target file or folders with immediate abort if a single test fails From f9d5b40034e769a28323cb3945bce83223364ac1 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sun, 28 Mar 2021 15:29:03 +0200 Subject: [PATCH 02/11] fix init problem --- lib/commands/init.js | 2 +- lib/setup/web-server.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/commands/init.js b/lib/commands/init.js index 7c933b0..c4c8717 100644 --- a/lib/commands/init.js +++ b/lib/commands/init.js @@ -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'] }, diff --git a/lib/setup/web-server.js b/lib/setup/web-server.js index a9693f5..b37fd03 100644 --- a/lib/setup/web-server.js +++ b/lib/setup/web-server.js @@ -1,6 +1,8 @@ import http from 'http'; import fs from 'fs/promises'; +// TODO: probably rewrite asset paths & expose needed assets under a middleware + export default async function setupWebServer(config = { httpPort: 1234, debug: false, watch: false, timeout: 10000 }, codeInputs = { QUnitCSS, allTestCode, mainHTML, staticHTMLs, dynamicContentHTMLs }, promise) { From d4b8892017a8c630e622a7c109d23f30fb7874fb Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sun, 28 Mar 2021 18:51:50 +0200 Subject: [PATCH 03/11] seperate 2 different run modes to functions --- lib/commands/run.js | 119 ++------------------------- lib/commands/run/tests-in-browser.js | 100 ++++++++++++++++++++++ lib/commands/run/tests-in-node.js | 14 ++++ 3 files changed, 119 insertions(+), 114 deletions(-) create mode 100644 lib/commands/run/tests-in-browser.js create mode 100644 lib/commands/run/tests-in-node.js diff --git a/lib/commands/run.js b/lib/commands/run.js index 6081e3e..3465f23 100644 --- a/lib/commands/run.js +++ b/lib/commands/run.js @@ -1,123 +1,14 @@ +import runTestsInNode from './run/tests-in-node.js'; +import runTestsInBrowser from './run/tests-in-browser.js'; // 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'; - - -const __dirname = dirname(fileURLToPath(import.meta.url)); - export default async function(config) { - const { browser, fileOrFolderInputs, projectRoot, timeout, output } = config; - - 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` - }); - - let [QUnitCSS, allTestCode] = 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) => { - let fileName = config.htmlPaths[index]; - let html = htmlBuffers[index].toString(); - - if (!html.includes('{{content}}')) { - result.staticHTMLs[fileName] = html; - } else { - result.dynamicContentHTMLs[fileName] = html; - } - - if (!result.mainHTML) { - let targetFileName = Object.keys(result.dynamicContentHTMLs)[0] || Object.keys(result.staticHTMLs)[0]; - - result.mainHTML = { - fileName: '/', - html: result.dynamicContentHTMLs[targetFileName] || result.staticHTMLs[targetFileName] - }; - } - - return result; - }, { - QUnitCSS: QUnitCSS.toString(), - allTestCode: allTestCode.toString(), - mainHTML: null, - staticHTMLs: {}, - dynamicContentHTMLs: {} - }); - - if (!codeInputs.mainHTML || !codeInputs.mainHTML.html) { - codeInputs.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); - - let TIME_TAKEN = MBER_TEST_TIME_COUNTER.stop() - - 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); - } + if (config.browser) { + await runTestsInBrowser(config.fileOrFolderInputs, config); // NOTE: cannot do test files individually/requires bundling? } 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'); - - QUnit.start(); - + await runTestsInNode(config.fileOrFolderInputs, config); // NOTE: can do test files individually } } diff --git a/lib/commands/run/tests-in-browser.js b/lib/commands/run/tests-in-browser.js new file mode 100644 index 0000000..c8ff7db --- /dev/null +++ b/lib/commands/run/tests-in-browser.js @@ -0,0 +1,100 @@ +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 TAPDisplayFinalResult from '../../tap/display-final-result.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function runTestsInBrowser(fileOrFolderInputs, config) { + let COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 }; + 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` + }); + + let [QUnitCSS, allTestCode] = 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) => { + let fileName = config.htmlPaths[index]; + let html = htmlBuffers[index].toString(); + + if (!html.includes('{{content}}')) { + result.staticHTMLs[fileName] = html; + } else { + result.dynamicContentHTMLs[fileName] = html; + } + + if (!result.mainHTML) { + let targetFileName = Object.keys(result.dynamicContentHTMLs)[0] || Object.keys(result.staticHTMLs)[0]; + + result.mainHTML = { + fileName: '/', + html: result.dynamicContentHTMLs[targetFileName] || result.staticHTMLs[targetFileName] + }; + } + + return result; + }, { + QUnitCSS: QUnitCSS.toString(), + allTestCode: allTestCode.toString(), + mainHTML: null, + staticHTMLs: {}, + dynamicContentHTMLs: {} + }); + + if (!codeInputs.mainHTML || !codeInputs.mainHTML.html) { + codeInputs.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); + + let TIME_TAKEN = MBER_TEST_TIME_COUNTER.stop() + + 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() + ]); + + return process.exit(COUNTER.failCount > 0 ? 1 : 0); + } + + return { server, browser, WebSocketServer }; +} diff --git a/lib/commands/run/tests-in-node.js b/lib/commands/run/tests-in-node.js new file mode 100644 index 0000000..b565673 --- /dev/null +++ b/lib/commands/run/tests-in-node.js @@ -0,0 +1,14 @@ +import parseFsInputs from '../../utils/parse-fs-inputs.js'; + +export default async function runTestsInNode(fileOrFolderInputs, config) { + global.testTimeout = config.timeout; + + const QUnit = (await import('../../setup/node-js-environment.js')).default; + const fsTree = await parseFsInputs(fileOrFolderInputs, {}, async(targetPath, fsEntry) => { + await import(targetPath); + fsEntry.executed = true; + }, config); + console.log('TAP version 13'); + + return QUnit.start(); +} From f0a83fc5e9971c0e317cc97990aa048240af003d Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sun, 18 Apr 2021 23:10:10 +0200 Subject: [PATCH 04/11] --browser --watch initial prototype --- TODO | 6 ++ lib/commands/run.js | 91 +++++++++++++++++++++++++++- lib/commands/run/tests-in-browser.js | 56 +++-------------- lib/commands/run/tests-in-node.js | 12 ++-- lib/setup/bind-server-to-port.js | 6 +- lib/setup/browser.js | 89 +++++++++++++++------------ lib/setup/file-watcher.js | 70 +++++++++++++++++++++ lib/setup/node-js-environment.js | 51 +++++++++------- lib/setup/web-server.js | 26 ++++---- lib/utils/parse-cli-flags.js | 6 +- lib/utils/parse-fs-inputs.js | 14 ++--- 11 files changed, 283 insertions(+), 144 deletions(-) create mode 100644 lib/setup/file-watcher.js diff --git a/TODO b/TODO index 9c994f9..45ba6d1 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,9 @@ + + +- 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 diff --git a/lib/commands/run.js b/lib/commands/run.js index 3465f23..6ca6345 100644 --- a/lib/commands/run.js +++ b/lib/commands/run.js @@ -1,14 +1,101 @@ +import fs from 'fs/promises'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; 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'; // let tree = Object.assign(tree, directoryReader(config, inputs, (file) => { // import(`${file}`); // })); // TODO: make it run an in-memory html if not exists +const __dirname = dirname(fileURLToPath(import.meta.url)); + export default async function(config) { + config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 }; + if (config.browser) { - await runTestsInBrowser(config.fileOrFolderInputs, config); // NOTE: cannot do test files individually/requires bundling? + let [QUnitCSS, ...htmlBuffers] = await Promise.all([ + fs.readFile(`${process.cwd()}/node_modules/qunit/qunit/qunit.css`), + ].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(); + + // TODO: here I could do html analysis to see which static js certain html points to? Complex algorithm + if (!html.includes('{{content}}')) { + result.staticHTMLs[fileName] = html; + } else { + result.dynamicContentHTMLs[fileName] = html; + } + + if (!result.mainHTML) { + let targetFileName = Object.keys(result.dynamicContentHTMLs)[0] || Object.keys(result.staticHTMLs)[0]; + + result.mainHTML = { + fileName: '/', + html: result.dynamicContentHTMLs[targetFileName] || result.staticHTMLs[targetFileName] + }; + } + + return result; + }, { + QUnitCSS: QUnitCSS.toString(), + allTestCode: null, + mainHTML: null, + staticHTMLs: {}, + dynamicContentHTMLs: {} + }); + if (!cachedContent.mainHTML || !cachedContent.mainHTML.html) { + cachedContent.mainHTML = { + fileName: '/', + html: (await fs.readFile(`${__dirname}/../boilerplates/setup/tests.hbs`)).toString() + } + } + + const { server, page, WebSocketServer, browser } = await runTestsInBrowser(config.fileOrFolderInputs, config, cachedContent); // NOTE: cannot do test files individually/requires bundling? + + if (config.watch) { + logWatcherAndKeyboardShortcutInfo(); + + await fileWatcher(config.fileOrFolderInputs, async (file) => { + config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 }; + // TODO: check if its html then run html otherwise run all html, how?? + await runTestsInBrowser([file], config, cachedContent, { server, page, WebSocketServer, browser }) + }, () => {}, config); + } } else { - await runTestsInNode(config.fileOrFolderInputs, config); // NOTE: can do test files individually + global.testTimeout = config.timeout; + + await setupNodeJSEnvironment(config); // NOTE = (await import('../setup/node-js-environment.js')).default(config); + await runTestsInNode(config.fileOrFolderInputs, window.QUnit, config); // NOTE: can do test files individually + + if (config.watch) { + + logWatcherAndKeyboardShortcutInfo(); + + let something = await fileWatcher(config.fileOrFolderInputs, async (file) => { + config.COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 }; + await setupNodeJSEnvironment(config); + await runTestsInNode([file], window.QUnit, config) + }, () => {}, config); + } + } +} + +function logWatcherAndKeyboardShortcutInfo() { + console.log('# Watching files.. Press "qa" to run all the tests, "ql" to run last failing test'); // NOTE: maybe add also qx to exit +} + +// Console.log(getEventColor(event), path.split(projectRoot)[1]); +function getEventColor(event) { + if (event === 'change') { + return chalk.yellow('CHANGED:'); + } else if (event === 'add' || event === 'addDir') { + return chalk.green('ADDED:'); + } else if (event === 'unlink' || event === 'unlinkDir') { + return chalk.red('REMOVED:'); } } diff --git a/lib/commands/run/tests-in-browser.js b/lib/commands/run/tests-in-browser.js index c8ff7db..1a7ef0e 100644 --- a/lib/commands/run/tests-in-browser.js +++ b/lib/commands/run/tests-in-browser.js @@ -8,14 +8,14 @@ import TAPDisplayFinalResult from '../../tap/display-final-result.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); -export default async function runTestsInBrowser(fileOrFolderInputs, config) { - let COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 }; +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); + // This prevents file cache most likely await esbuild.build({ stdin: { contents: Object.keys(fsTree).reduce((result, fileAbsolutePath) => { @@ -28,49 +28,9 @@ export default async function runTestsInBrowser(fileOrFolderInputs, config) { outfile: `${projectRoot}/${output}/tests.js` }); - let [QUnitCSS, allTestCode] = 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 + 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 codeInputs = config.htmlPaths.reduce((result, htmlPath, index) => { - let fileName = config.htmlPaths[index]; - let html = htmlBuffers[index].toString(); - - if (!html.includes('{{content}}')) { - result.staticHTMLs[fileName] = html; - } else { - result.dynamicContentHTMLs[fileName] = html; - } - - if (!result.mainHTML) { - let targetFileName = Object.keys(result.dynamicContentHTMLs)[0] || Object.keys(result.staticHTMLs)[0]; - - result.mainHTML = { - fileName: '/', - html: result.dynamicContentHTMLs[targetFileName] || result.staticHTMLs[targetFileName] - }; - } - - return result; - }, { - QUnitCSS: QUnitCSS.toString(), - allTestCode: allTestCode.toString(), - mainHTML: null, - staticHTMLs: {}, - dynamicContentHTMLs: {} - }); - - if (!codeInputs.mainHTML || !codeInputs.mainHTML.html) { - codeInputs.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(); @@ -79,13 +39,13 @@ export default async function runTestsInBrowser(fileOrFolderInputs, config) { stop: () => +(new Date()) - (+startTime) }; })(); - let { browser, server, WebSocketServer } = await setupBrowser(COUNTER, config, codeInputs); + + let { browser, server, WebSocketServer } = await setupBrowser(config, cachedContent, connections); // NOTE: this is cachedContent let TIME_TAKEN = MBER_TEST_TIME_COUNTER.stop() - TAPDisplayFinalResult(COUNTER, TIME_TAKEN); + TAPDisplayFinalResult(config.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(), @@ -93,7 +53,7 @@ export default async function runTestsInBrowser(fileOrFolderInputs, config) { WebSocketServer.close() ]); - return process.exit(COUNTER.failCount > 0 ? 1 : 0); + return process.exit(config.COUNTER.failCount > 0 ? 1 : 0); } return { server, browser, WebSocketServer }; diff --git a/lib/commands/run/tests-in-node.js b/lib/commands/run/tests-in-node.js index b565673..c28ac2e 100644 --- a/lib/commands/run/tests-in-node.js +++ b/lib/commands/run/tests-in-node.js @@ -1,14 +1,16 @@ import parseFsInputs from '../../utils/parse-fs-inputs.js'; -export default async function runTestsInNode(fileOrFolderInputs, config) { - global.testTimeout = config.timeout; - - const QUnit = (await import('../../setup/node-js-environment.js')).default; +export default async function runTestsInNode(fileOrFolderInputs, QUnit, config) { + window.QUnit.config = Object.assign({}, window.oldQunitConfig); + window.location = window.location; const fsTree = await parseFsInputs(fileOrFolderInputs, {}, async(targetPath, fsEntry) => { + console.log('importing'); await import(targetPath); fsEntry.executed = true; }, config); console.log('TAP version 13'); - return QUnit.start(); + // globalStartCalled = false; + QUnit.start(); + console.log('called start'); } diff --git a/lib/setup/bind-server-to-port.js b/lib/setup/bind-server-to-port.js index 73eab6e..5db633c 100644 --- a/lib/setup/bind-server-to-port.js +++ b/lib/setup/bind-server-to-port.js @@ -3,7 +3,7 @@ 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'); @@ -11,7 +11,7 @@ export default async function bindServerToPort(server, WebSocketServer, COUNTER, const { event, details } = JSON.parse(message); if (event === 'testEnd') { - TAPDisplayTestResult(COUNTER, details) + TAPDisplayTestResult(config.COUNTER, details) } }); }); @@ -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); }); diff --git a/lib/setup/browser.js b/lib/setup/browser.js index 3a67237..87eaa0b 100644 --- a/lib/setup/browser.js +++ b/lib/setup/browser.js @@ -3,69 +3,73 @@ import setupWebServer from './web-server.js'; import setupWebsocketServer from './websocket-server.js'; import bindServerToPort from './bind-server-to-port.js'; -export default async function setupBrowser(COUNTER, config = { +export default async function setupBrowser(config = { httpPort: 1234, debug: false, watch: false, timeout: 10000 -}, codeInputs = { QUnitCSS, allTestCode, mainHTML, staticHTMLs, dynamicContentHTMLs } ) { - let { mainHTML, staticHTMLs, dynamicContentHTMLs} = codeInputs; - let { server, htmlInputs } = await new Promise((resolve, reject) => setupWebServer(config, codeInputs, { resolve, reject })); - let WebSocketServer = await new Promise((resolve, reject) => setupWebsocketServer(server, { resolve, reject })); - - server = await new Promise((resolve, reject) => bindServerToPort(server, WebSocketServer, COUNTER, config, { resolve, reject })); - - const browser = await Puppeteer.launch({ +}, cachedContent = { QUnitCSS, allTestCode, mainHTML, staticHTMLs, dynamicContentHTMLs }, connections = {}) { + let { mainHTML, staticHTMLs, dynamicContentHTMLs } = cachedContent; + let server = connections.server || + await new Promise((resolve, reject) => setupWebServer(config, cachedContent, { resolve, reject })); + let WebSocketServer = connections.WebSocketServer || + await new Promise((resolve, reject) => setupWebsocketServer(server, { resolve, reject })); + connections.server || + await new Promise((resolve, reject) => bindServerToPort(server, WebSocketServer, config, { resolve, reject })); + let browser = connections.browser || await Puppeteer.launch({ executablePath: process.env.CHROME_BIN || null, headless: true, args: ['--no-sandbox', '--disable-gpu', '--remote-debugging-port=0', '--window-size=1440,900'] }); - const page = await browser.newPage(); + let page = connections.page || await browser.newPage(); - page.on('console', async (msg) => { - if (config.debug) { - const args = await Promise.all(msg.args().map((arg) => turnToObjects(arg))); + if (!connections.page) { + page.on('console', async (msg) => { + if (config.debug) { + const args = await Promise.all(msg.args().map((arg) => turnToObjects(arg))); - console.log(...args); - } - }); - page.on('error', (msg) => { - try { - throw error; - } catch (e) { - console.error(e, e.stack); - console.log(e, e.stack); - } - }); - page.on('pageerror', async (error) => { - try { - throw error; - } catch (e) { - console.log(e.toString()); - console.error(e.toString()); - } - }); + console.log(...args); + } + }); + page.on('error', (msg) => { + try { + throw error; + } catch (e) { + console.error(e, e.stack); + console.log(e, e.stack); + } + }); + page.on('pageerror', async (error) => { + try { + throw error; + } catch (e) { + console.log(e.toString()); + console.error(e.toString()); + } + }); + } + // TODO: Current logic would run all static and dynamic HTMLs on each file change // NOTE: this type of checking below ignores static file js/css references let arrayOfHTMLs = Object.keys(dynamicContentHTMLs).concat(Object.keys(staticHTMLs)); if (arrayOfHTMLs.length === 0) { arrayOfHTMLs = ['/'] } + // TODO: this runs all JS inside all htmls currently! await Promise.all(arrayOfHTMLs.map(async (htmlPath) => { - await runTestInsideHTMLFile(htmlPath, server, page, config.timeout, arrayOfHTMLs) + // NOTE: we are blocking each html here on purpose + await runTestInsideHTMLFile(htmlPath, server, page, config.timeout) })); - return { browser, server, WebSocketServer }; + return { browser, page, server, WebSocketServer }; } function turnToObjects(jsHandle) { return jsHandle.jsonValue(); } -async function runTestInsideHTMLFile(filePath, server, page, timeout, arrayOfHTMLs) { +async function runTestInsideHTMLFile(filePath, server, page, timeout) { let QUNIT_RESULT; try { - if (arrayOfHTMLs.length > 1) { - console.log(`# Running: ${filePath}`); - } + console.log(`# Running: ${filePath}`); await page.goto(`http://localhost:${server.address().port}${filePath}`, { timeout: 0 }); await page.waitForFunction(`window.testTimeout >= ${timeout}`, { timeout: 0 }); @@ -79,15 +83,20 @@ async function runTestInsideHTMLFile(filePath, server, page, timeout, arrayOfHTM console.log('BROWSER: runtime error thrown during executing tests'); console.error('BROWSER: runtime error thrown during executing tests'); - await new Promise((resolve, reject) => setTimeout(() => resolve(process.exit(1)), 100)); + await failOnNonWatchMode(config.watch); } else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) { console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`); console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`); - await new Promise((resolve, reject) => setTimeout(() => resolve(process.exit(1)), 100)); + await failOnNonWatchMode(config.watch); } } +async function failOnNonWatchMode(watchMode = false) { + if (!watchMode) { + await new Promise((resolve, reject) => setTimeout(() => resolve(process.exit(1)), 100)); + } +} // function turnMStoSecond(timeInMS) { // return (timeInMS / 1000).toFixed(2); // } diff --git a/lib/setup/file-watcher.js b/lib/setup/file-watcher.js new file mode 100644 index 0000000..30da4a2 --- /dev/null +++ b/lib/setup/file-watcher.js @@ -0,0 +1,70 @@ +import chokidar from 'chokidar'; +import kleur from 'kleur'; + +// TODO: if needed add reload with broadcastMessage(--autorefresh) +export default async function fileWatcher(fileOrFolderInputs, closure, closureOnFinish, config) { + // NOTE: get from the closure + + return { + watcher: buildWatchers(fileOrFolderInputs, closure, closureOnFinish, config), + killWatchers: () => cleanWatchers(watcher) + }; + + function buildWatchers(fileOrFolderInputs, closure, closureOnFinish, config) { + return fileOrFolderInputs.reduce((watcher, input) => { + return Object.assign(watcher, { [input]: watch(input, (path, event) => closure(path), closureOnFinish) }); + }, {}); + } + + function watch(watchPath, buildFunction, callback) { + console.log(watchPath); + return chokidar.watch(watchPath, { ignoreInitial: true }).on('all', (event, path) => { + if (!global.chokidarBuild) { + global.chokidarBuild = true; + + console.log(''); + console.log('#', getEventColor(event), path.split(config.projectRoot)[1]); + console.log(''); + + let result = pathIsForBuild(path) ? buildFunction(path) : null; + + if (!(result instanceof Promise)) { + global.chokidarBuild = false; + + return result; + } + + result + .then(() => { + callback ? callback(path, event) : null; + }) + .catch(() => { + // TODO: make an index.html to display the error + // error type has to be derived from the error! + }) + .finally(() => (global.chokidarBuild = false)); + } + }); + } + + function cleanWatchers(watcher) { + Object.keys(watcher).forEach((watcherKey) => watcher[watcherKey].close()); + + return watcher; + } + + function pathIsForBuild(path) { + return ['.js', '.ts', '.html'].some((extension) => path.endsWith(extension)); + } + + function getEventColor(event) { + if (event === 'change') { + return kleur.yellow('CHANGED:'); + } else if (event === 'add' || event === 'addDir') { + return kleur.green('ADDED:'); + } else if (event === 'unlink' || event === 'unlinkDir') { + return kleur.red('REMOVED:'); + } + } +} + diff --git a/lib/setup/node-js-environment.js b/lib/setup/node-js-environment.js index 9710495..f2157fb 100644 --- a/lib/setup/node-js-environment.js +++ b/lib/setup/node-js-environment.js @@ -4,33 +4,38 @@ import indentString from 'indent-string'; import TAPDisplayFinalResult from '../tap/display-final-result.js'; import TAPDisplayTestResult from '../tap/display-test-result.js'; -let COUNTER = { testCount: 0, failCount: 0, skipCount: 0, passCount: 0 }; +export default function setupNodeJSEnvironment(config) { + global.window = global; + window.QUnit = null; + window.QUnit = QUnit; + window.QUnit.config.autostart = false; + window.QUnit.config.testTimeout = global.testTimeout; -global.window = global; -window.QUnit = QUnit; -window.QUnit.config.autostart = false; -window.QUnit.config.testTimeout = global.testTimeout; + window.QUNIX_TEST_TIME_COUNTER = (function() { // NOTE: might be needed for failFast option timeTaken calculation + const startTime = new Date(); -window.QUNIX_TEST_TIME_COUNTER = (function() { // NOTE: might be needed for failFast option timeTaken calculation - const startTime = new Date(); + return { + start: startTime, + stop: () => +(new Date()) - (+startTime) + }; + })(); - return { - start: startTime, - stop: () => +(new Date()) - (+startTime) - }; -})(); + window.QUnit.on('testEnd', (details) => { + TAPDisplayTestResult(config.COUNTER, details) + }); + window.QUnit.done((details) => { + window.QUNIT_RESULT = Object.assign(details, { + timeTaken: window.QUNIX_TEST_TIME_COUNTER.stop() + }); + TAPDisplayFinalResult(config.COUNTER, details.timeTaken); -window.QUnit.on('testEnd', (details) => { - TAPDisplayTestResult(COUNTER, details) -}); -window.QUnit.done((details) => { - window.QUNIT_RESULT = Object.assign(details, { - timeTaken: window.QUNIX_TEST_TIME_COUNTER.stop() + if (!config.watch) { + process.exit(config.COUNTER.failCount > 0 ? 1 : 0); + } }); - TAPDisplayFinalResult(COUNTER, details.timeTaken); - process.exit(COUNTER.failCount > 0 ? 1 : 0); -}); -process.on('uncaughtException', (err) => console.error(err)); + process.on('uncaughtException', (err) => console.error(err)); + window.oldQunitConfig = Object.assign({}, window.QUnit.config); -export default window.QUnit; + return window.QUnit; +} diff --git a/lib/setup/web-server.js b/lib/setup/web-server.js index b37fd03..7cc7186 100644 --- a/lib/setup/web-server.js +++ b/lib/setup/web-server.js @@ -5,9 +5,7 @@ import fs from 'fs/promises'; export default async function setupWebServer(config = { httpPort: 1234, debug: false, watch: false, timeout: 10000 -}, codeInputs = { QUnitCSS, allTestCode, mainHTML, staticHTMLs, dynamicContentHTMLs }, promise) { - let { QUnitCSS, allTestCode, mainHTML, staticHTMLs, dynamicContentHTMLs} = codeInputs; - +}, cachedContent, promise) { // TODO: occasionally wrong content is served!! const server = http.createServer((req, res) => { let TEST_RUNTIME_TO_INJECT = ``; - if (req.url === '../node_modules/qunit/qunit/qunit.css') { + if (['../node_modules/qunit/qunit/qunit.css', './node_modules/qunit/qunit/qunit.css', '/node_modules/qunit/qunit/qunit.css'].includes(req.url)) { res.writeHead(200, { 'Content-Type': 'text/css' }); return res.end(cachedContent.QUnitCSS, 'utf-8'); } From 33375d6a5a389a258b63da2c228800c5aeae430f Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Thu, 22 Apr 2021 20:24:45 +0200 Subject: [PATCH 11/11] move file-watcher util func to correct place --- lib/setup/file-watcher.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/setup/file-watcher.js b/lib/setup/file-watcher.js index f1b1be9..e18edac 100644 --- a/lib/setup/file-watcher.js +++ b/lib/setup/file-watcher.js @@ -44,25 +44,24 @@ export default async function fileWatcher(fileOrFolderInputs, closure, closureOn } }); } +} - function cleanWatchers(watcher) { - Object.keys(watcher).forEach((watcherKey) => watcher[watcherKey].close()); +function cleanWatchers(watcher) { + Object.keys(watcher).forEach((watcherKey) => watcher[watcherKey].close()); - return watcher; - } + return watcher; +} - function pathIsForBuild(path) { - return ['.js', '.ts', '.html'].some((extension) => path.endsWith(extension)); - } +function pathIsForBuild(path) { + return ['.js', '.ts', '.html'].some((extension) => path.endsWith(extension)); +} - function getEventColor(event) { - if (event === 'change') { - return kleur.yellow('CHANGED:'); - } else if (event === 'add' || event === 'addDir') { - return kleur.green('ADDED:'); - } else if (event === 'unlink' || event === 'unlinkDir') { - return kleur.red('REMOVED:'); - } +function getEventColor(event) { + if (event === 'change') { + return kleur.yellow('CHANGED:'); + } else if (event === 'add' || event === 'addDir') { + return kleur.green('ADDED:'); + } else if (event === 'unlink' || event === 'unlinkDir') { + return kleur.red('REMOVED:'); } } -