Skip to content

Commit

Permalink
Implement 'minimal' reporter and collect window.onerror/console.error…
Browse files Browse the repository at this point in the history
…/warn

errorString() is based on QUnit, of which the key component is this
patch by Peter Wagenet qunitjs/qunit@9ec5593257.

Co-authored-by: Peter Wagenet <[email protected]>
  • Loading branch information
Krinkle and wagenet committed Jan 24, 2025
1 parent da1e629 commit 68407d9
Show file tree
Hide file tree
Showing 30 changed files with 1,032 additions and 290 deletions.
17 changes: 13 additions & 4 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,18 @@ jobs:

- run: npm test

- name: Check system browsers
run: node bin/qtap.js -v -b firefox -b chrome -b chromium -b edge test/pass.html
- name: Test local browsers (Firefox)
run: node bin/qtap.js -b firefox -r none -v --timeout 60 test/fixtures/pass.html

- name: Check system browsers (Safari)
- name: Test local browsers (Chrome)
run: node bin/qtap.js -b chrome -r none -v --timeout 60 test/fixtures/pass.html

- name: Test local browsers (Chromium)
run: node bin/qtap.js -b chromium -r none -v --timeout 60 test/fixtures/pass.html

- name: Test local browsers (Edge)
run: node bin/qtap.js -b edge -r none -v --timeout 60 test/fixtures/pass.html

- name: Test local browsers (Safari)
if: ${{ runner.os == 'macOS' }}
run: node bin/qtap.js -v -b safari test/pass.html
run: node bin/qtap.js -b safari -r none -v --timeout 60 test/fixtures/pass.html
21 changes: 9 additions & 12 deletions bin/qtap.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ program
}
return num;
},
30
5
)
.option('--connect-timeout <number>',
'How many seconds a browser may take to start up.',
Expand All @@ -49,8 +49,10 @@ program
},
60
)
.option('-r, --reporter <reporter>', 'One of "minimal", "dynamic", or "none".', 'minimal')
.option('-w, --watch', 'Watch files for changes and re-run the test suite.')
.option('-v, --verbose', 'Enable verbose debug logging.')
.option('-d, --debug', 'Enable debug mode (non-headless browser, and verbose logging).')
.option('-v, --verbose', 'Enable verbose logging.')
.option('-V, --version', 'Display version number.')
.helpOption('-h, --help', 'Display this usage information.')
.showHelpAfterError()
Expand All @@ -66,21 +68,16 @@ if (opts.version) {
} else if (!program.args.length) {
program.help();
} else {
process.on('unhandledRejection', (reason) => {
console.error(reason);
});
process.on('uncaughtException', (error) => {
console.error(error);
});

try {
const exitCode = await qtap.run(opts.browser, program.args, {
const result = await qtap.runWaitFor(opts.browser, program.args, {
config: opts.config,
timeout: opts.timeout,
connectTimeout: opts.connectTimeout,
verbose: opts.verbose
reporter: opts.reporter,
debug: opts.debug || (process.env.QTAP_DEBUG === '1'),
verbose: opts.debug || opts.verbose,
});
process.exit(exitCode);
process.exit(result.exitCode);
} catch (e) {
console.error(e);
process.exit(1);
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,26 @@
},
"main": "src/qtap.js",
"scripts": {
"unit": "qunit",
"integration-basic": "node bin/qtap.js -v test/pass.html",
"unit": "qunit test/*.js",
"integration-basic": "node bin/qtap.js -r none -v test/fixtures/pass.html",
"lint": "eslint --cache .",
"lint-fix": "eslint --cache --fix .",
"types": "tsc",
"test": "npm run -s integration-basic && npm run -s unit && npm run lint && npm run types"
},
"dependencies": {
"commander": "12.1.0",
"kleur": "4.1.5",
"tap-parser": "18.0.0",
"which": "5.0.0"
"which": "5.0.0",
"yaml": "^2.4.1"
},
"devDependencies": {
"@types/node": "22.10.5",
"@types/which": "3.0.4",
"eslint": "~8.57.1",
"eslint-config-semistandard": "~17.0.0",
"eslint-plugin-qunit": "^8.1.2",
"qunit": "2.23.1",
"qunit": "2.24.0",
"semistandard": "~17.0.0",
"typescript": "5.7.3"
},
Expand Down
88 changes: 43 additions & 45 deletions src/browsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import safari from './safari.js';
import { concatGenFn, CommandNotFoundError, LocalBrowser } from './util.js';
/** @import { Logger, Browser } from './qtap.js' */

const QTAP_DEBUG = process.env.QTAP_DEBUG === '1';

// - use Set to remove duplicate values, because `PROGRAMFILES` and `ProgramW6432` are often
// both "C:\Program Files", which, we'd check three times otherwise.
// - it is important that this preserves order of precedence.
Expand Down Expand Up @@ -114,11 +112,13 @@ function * getEdgePaths () {
* @param {string} url
* @param {Object<string,AbortSignal>} signals
* @param {Logger} logger
* @param {boolean} debugMode
*/
async function firefox (url, signals, logger) {
async function firefox (url, signals, logger, debugMode) {
const profileDir = LocalBrowser.makeTempDir(signals, logger);
const args = [url, '-profile', profileDir, '-no-remote', '-wait-for-browser'];
if (!QTAP_DEBUG) {
if (!debugMode) {
firefox.displayName = 'Headless Firefox';
args.push('-headless');
}

Expand Down Expand Up @@ -157,54 +157,52 @@ async function firefox (url, signals, logger) {
firefox.displayName = 'Firefox';

/**
* @param {Function} getPaths
* @param {string} url
* @param {Object<string,AbortSignal>} signals
* @param {Logger} logger
* @param {string} displayName
* @param {() => Generator} getPaths
* @return {Browser}
*/
async function chromiumGeneric (getPaths, url, signals, logger) {
// https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
const dataDir = LocalBrowser.makeTempDir(signals, logger);
const args = [
'--user-data-dir=' + dataDir,
'--no-default-browser-check',
'--no-first-run',
'--disable-default-apps',
'--disable-popup-blocking',
'--disable-translate',
'--disable-background-timer-throttling',
...(QTAP_DEBUG ? [] : [
'--headless',
'--disable-gpu',
'--disable-dev-shm-usage'
]),
...(process.env.CHROMIUM_FLAGS ? process.env.CHROMIUM_FLAGS.split(/\s+/) : (
process.env.CI ? ['--no-sandbox'] : [])
),
url
];
await LocalBrowser.spawn(getPaths(), args, signals, logger);
function makeChromium (displayName, getPaths) {
/** @type {Browser} - https://github.com/microsoft/TypeScript/issues/22063 */
const chromium = async function (url, signals, logger, debugMode) {
chromium.displayName = debugMode ? displayName : `Headless ${displayName}`;
// https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
const dataDir = LocalBrowser.makeTempDir(signals, logger);
const args = [
'--user-data-dir=' + dataDir,
'--no-default-browser-check',
'--no-first-run',
'--disable-default-apps',
'--disable-popup-blocking',
'--disable-translate',
'--disable-background-timer-throttling',
...(debugMode ? [] : [
'--headless',
'--disable-gpu',
'--disable-dev-shm-usage'
]),
...(process.env.CHROMIUM_FLAGS ? process.env.CHROMIUM_FLAGS.split(/\s+/) : (
process.env.CI ? ['--no-sandbox'] : [])
),
url
];
await LocalBrowser.spawn(getPaths(), args, signals, logger);
};
return chromium;
}

const chrome = chromiumGeneric.bind(null, getChromePaths);
chrome.displayName = 'Chrome';

const chromium = chromiumGeneric.bind(null, getChromiumPaths);
chromium.displayName = 'Chromium';

const edge = chromiumGeneric.bind(null, getEdgePaths);
edge.displayName = 'Edge';

const chromiumAny = chromiumGeneric.bind(null, concatGenFn(getChromiumPaths, getChromePaths, getEdgePaths));
chromiumAny.displayName = 'Chromium';
const chrome = makeChromium('Chrome', getChromePaths);
const chromium = makeChromium('Chromium', getChromiumPaths);
const edge = makeChromium('Edge', getEdgePaths);
const chromiumAny = makeChromium('Chromium', concatGenFn(getChromiumPaths, getChromePaths, getEdgePaths));

/** @type {Browser} - https://github.com/microsoft/TypeScript/issues/22063 */
const detect = async function (url, signals, logger) {
const detect = async function (url, signals, logger, debugMode) {
for (const fn of [firefox, chrome, chromium, edge, safari]) {
detect.displayName = fn.displayName || fn.name;
logger.debug('detect_try', detect.displayName);
logger.debug('detect_try', fn.name);
try {
await fn(url, signals, logger);
const browerPromise = fn(url, signals, logger, debugMode);
detect.displayName = fn.displayName || fn.name;
await browerPromise;
return;
} catch (e) {
if (e instanceof CommandNotFoundError) {
Expand Down
109 changes: 75 additions & 34 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,102 @@
/* eslint-disable no-undef, no-var -- Browser code */
/* eslint-disable no-var -- Browser code */
/* global XMLHttpRequest, QUnit */
// @ts-nocheck

export function fnToStr (fn, qtapUrl) {
export function fnToStr (fn, qtapTapUrl, qtapStderrUrl) {
return fn
.toString()
.replace(/\/\/.+$/gm, '')
.replace(/\n|^\s+/gm, ' ')
.replace(
"'{{QTAP_URL}}'",
JSON.stringify(qtapUrl)
/'{{QTAP_TAP_URL}}'/g,
JSON.stringify(qtapTapUrl)
)
.replace(
/'{{QTAP_STDERR_URL}}'/g,
JSON.stringify(qtapStderrUrl)
);
}

// See ARCHITECTURE.md#qtap-internal-client-send
export function qtapClientHead () {
// Support QUnit 3.0+: Enable TAP reporter, declaratively.
// Support QUnit 2.24+: Enable TAP reporter, declaratively.
window.qunit_config_reporters_tap = true;

// See ARCHITECTURE.md#qtap-internal-client-send
var qtapNativeLog = console.log;
var qtapBuffer = '';
var qtapShouldSend = true;
function qtapSend () {
var body = qtapBuffer;
qtapBuffer = '';
qtapShouldSend = false;

var xhr = new XMLHttpRequest();
xhr.onload = xhr.onerror = () => {
qtapShouldSend = true;
if (qtapBuffer) {
qtapSend();
// Support IE 9: console.log.apply is undefined.
// Don't bother with Function.apply.call. Skip super call instead.
var console = window.console || (window.console = {});
var log = console.log && console.log.apply ? console.log : function () {};
var warn = console.warn && console.warn.apply ? console.warn : function () {};
var error = console.error && console.error.apply ? console.error : function () {};

function createBufferedWrite (url) {
var buffer = '';
var isSending = false;
function send () {
isSending = true;

var body = buffer;
buffer = '';

var xhr = new XMLHttpRequest();
xhr.onload = xhr.onerror = () => {
isSending = false;
if (buffer) {
send();
}
};
xhr.open('POST', url, true);
xhr.send(body);
}
return function write (str) {
buffer += str + '\n';
if (!isSending) {
isSending = true;
setTimeout(send, 0);
}
};
xhr.open('POST', '{{QTAP_URL}}', true);
xhr.send(body);
}
console.log = function qtapLog (str) {

var writeTap = createBufferedWrite('{{QTAP_TAP_URL}}');
var writeConsoleError = createBufferedWrite('{{QTAP_STDERR_URL}}');

console.log = function qtapConsoleLog (str) {
if (typeof str === 'string') {
qtapBuffer += str + '\n';
if (qtapShouldSend) {
qtapShouldSend = false;
setTimeout(qtapSend, 0);
}
writeTap(str);
}
return qtapNativeLog.apply(this, arguments);
return log.apply(console, arguments);
};

// TODO: Forward console.warn, console.error, and onerror to server.
// TODO: Report window.onerror as TAP comment, visible by default.
// TODO: Report console.warn/console.error in --verbose mode.
window.addEventListener('error', function (error) {
console.log('Script error: ' + (error.message || 'Unknown error'));
console.warn = function qtapConsoleWarn (str) {
writeConsoleError(String(str));
return warn.apply(console, arguments);
};

console.error = function qtapConsoleError (str) {
writeConsoleError(String(str));
return error.apply(console, arguments);
};

function errorString (error) {
var str = String(error);
if (str.slice(0, 7) === '[object') {
// Based on https://es5.github.io/#x15.11.4.4
return (error.name || 'Error') + (error.message ? (': ' + error.message) : '');
}
return str;
}

window.addEventListener('error', function (event) {
var str = event.error ? errorString(event.error) : (event.message || 'Script error');
if (event.filename && event.lineno) {
str += '\n at ' + event.filename + ':' + event.lineno;
}
writeConsoleError(str);
});
}

export function qtapClientBody () {
// Support QUnit 2.16 - 2.22: Enable TAP reporter, procedurally.
// Support QUnit 2.16 - 2.23: Enable TAP reporter, procedurally.
if (typeof QUnit !== 'undefined' && QUnit.reporters && QUnit.reporters.tap && (!QUnit.config.reporters || !QUnit.config.reporters.tap)) {
QUnit.reporters.tap.init(QUnit);
}
Expand Down
Loading

0 comments on commit 68407d9

Please sign in to comment.