Migrate some tests to the new htmlRunner

Fix events.get_timeStamp (was events.get_timestamp, wrong casing).

Rename `newRunner` to `htmlRunner`.

move tests to src/tests (from src/browser/tests). src/runtime and possibly other
parts might want to have html tests too.
This commit is contained in:
Karl Seguin
2025-09-02 10:38:27 +08:00
parent e486f28a41
commit 81766c8517
22 changed files with 317 additions and 300 deletions

5
src/tests/browser.html Normal file
View File

@@ -0,0 +1,5 @@
<script src="testing.js"></script>
<script id=intl>
// this will crash if ICU isn't properly configured / ininitialized
testing.expectEqual("[object Intl.DateTimeFormat]", new Intl.DateTimeFormat().toString());
</script>

25
src/tests/crypto.html Normal file
View File

@@ -0,0 +1,25 @@
<script src="testing.js"></script>
<script id=crypto>
const a = crypto.randomUUID();
const b = crypto.randomUUID();
testing.expectEqual(36, a.length);
testing.expectEqual(36, b.length);
testing.expectEqual(false, a == b)
testing.expectError('Error: QuotaExceededError', () => {
crypto.getRandomValues(new BigUint64Array(8193));
});
let r1 = new Int32Array(5)
let r2 = crypto.getRandomValues(r1)
testing.expectEqual(5, new Set(r1).size);
testing.expectEqual(5, new Set(r2).size);
testing.expectEqual(true, r1.every((v, i) => v === r2[i]));
var r3 = new Uint8Array(16);
let r4 = crypto.getRandomValues(r3);
r4[6] = 10;
testing.expectEqual(10, r4[6]);
testing.expectEqual(10, r3[6]);
</script>

5
src/tests/css.html Normal file
View File

@@ -0,0 +1,5 @@
<script src="testing.js"></script>
<script id=support>
testing.expectEqual(true, CSS.supports('display: flex'));
testing.expectEqual(true, CSS.supports('text-decoration-style', 'blink'));
</script>

View File

@@ -0,0 +1,17 @@
<script src="../testing.js"></script>
<script id=decoder>
let d1 = new TextDecoder();
testing.expectEqual('utf-8', d1.encoding);
testing.expectEqual(false, d1.fatal);
testing.expectEqual(false, d1.ignoreBOM);
testing.expectEqual('𠮷', d1.decode(new Uint8Array([240, 160, 174, 183])));
testing.expectEqual('𠮷', d1.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 240, 160, 174, 183])));
testing.expectEqual('<27>2', d1.decode(new Uint8Array([249, 50])));
let d2 = new TextDecoder('utf8', {fatal: true})
testing.expectError('Error: InvalidUtf8', () => {
let data = new Uint8Array([240, 240, 160, 174, 183]);
d2.decode(data);
});
</script>

View File

@@ -0,0 +1,13 @@
<script src="../testing.js"></script>
<script id=encoder>
var encoder = new TextEncoder();
testing.expectEqual('utf-8', encoder.encoding);
testing.expectEqual([226, 130, 172], Array.from(encoder.encode('€')));
// Invalid utf-8 sequence.
// Browsers give a different result for this, they decode it to:
// 50, 50, 54, 44, 52, 48, 44, 49, 54, 49
testing.expectError('Error: InvalidUtf8', () => {
encoder.encode(new Uint8Array([0xE2,0x28,0xA1]));
});
</script>

View File

@@ -0,0 +1,16 @@
<script src="../testing.js"></script>
<script id=custom>
let capture = null;
const el = document.createElement('div');
el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)});
el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)});
el.dispatchEvent(new CustomEvent('c1'));
testing.expectEqual("c1-null", capture);
el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));
testing.expectEqual("c1-123", capture);
el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));
testing.expectEqual("c2-9000", capture);
</script>

138
src/tests/events/event.html Normal file
View File

@@ -0,0 +1,138 @@
<script src="../testing.js"></script>
<div id=content>
<p id="para"></p>
</div>
<script id=dispatch>
const startTime = new Date().getTime();
let content = $('#content');
// let para = document.getElementById('para');
var nb = 0;
var evt = null;
const incrementCallback = function(e) {
evt = e;
nb += 1;
e.preventDefault();
}
content.addEventListener('dispatch', incrementCallback);
content.dispatchEvent(new Event('dispatch', {bubbles: true, cancelable: true}));
testing.expectEqual(1, nb);
testing.expectEqual(content, evt.target);
testing.expectEqual(true, evt.bubbles);
testing.expectEqual(true, evt.cancelable);
testing.expectEqual(true, evt.defaultPrevented);
testing.expectEqual(true, evt.isTrusted);
testing.expectEqual(true, evt.timeStamp >= Math.floor(startTime/1000));
</script>
<script id=propogate>
nb = 0;
let para = $('#para');
// the stop listener is capturing, so it propogates down
content.addEventListener('stop',function(e) {
e.stopPropagation();
nb += 1;
}, true)
para.addEventListener('stop',function(e) {
nb += 10;
});
para.dispatchEvent(new Event('stop'));
// didn't propogate down (because of capturing) to para handler
testing.expectEqual(1, nb);
</script>
<script id=immediate>
nb = 0;
content.addEventListener('immediate', function(e) {
e.stopImmediatePropagation();
nb += 1;
});
// the following event listener will not be invoked
content.addEventListener('immediate', function(e) {
nb += 10;
});
content.dispatchEvent(new Event('immediate'));
testing.expectEqual(1, nb);
</script>
<script id=legacy>
nb = 0;
content.addEventListener('legacy', incrementCallback);
let evtLegacy = document.createEvent('Event');
evtLegacy.initEvent('legacy');
content.dispatchEvent(evtLegacy);
testing.expectEqual(1, nb);
</script>
<script id=removeListener>
nb = 0;
document.addEventListener('count', incrementCallback);
document.removeEventListener('count', incrementCallback);
document.dispatchEvent(new Event('count'));
testing.expectEqual(0, nb);
</script>
<script id=once>
document.addEventListener('count', incrementCallback, {once: true});
document.dispatchEvent(new Event('count'));
document.dispatchEvent(new Event('count'));
document.dispatchEvent(new Event('count'));
testing.expectEqual(1, nb);
</script>
<script id=abortController>
nb = 0;
let ac = new AbortController()
document.addEventListener('count', incrementCallback, {signal: ac.signal})
document.dispatchEvent(new Event('count'));
document.dispatchEvent(new Event('count'));
ac.abort();
document.dispatchEvent(new Event('count'));
testing.expectEqual(2, nb);
document.removeEventListener('count', incrementCallback);
</script>
<script id=composedPath>
testing.expectEqual([], new Event('').composedPath());
let div1 = document.createElement('div');
let sr1 = div1.attachShadow({mode: 'open'});
sr1.innerHTML = "<p id=srp1></p>";
document.getElementsByTagName('body')[0].appendChild(div1);
let cp = null;
const shadowCallback = function(e) {
cp = e.composedPath().map((n) => n.id || n.nodeName || n.toString());
}
div1.addEventListener('click', shadowCallback);
sr1.getElementById('srp1').click();
testing.expectEqual(
['srp1', '#document-fragment', 'DIV', 'BODY', 'HTML', '#document', '[object Window]'],
cp
);
let div2 = document.createElement('div');
let sr2 = div2.attachShadow({mode: 'closed'});
sr2.innerHTML = "<p id=srp2></p>";
document.getElementsByTagName('body')[0].appendChild(div2);
cp = null;
div2.addEventListener('click', shadowCallback);
sr2.getElementById('srp2').click();
testing.expectEqual(
['DIV', 'BODY', 'HTML', '#document', '[object Window]'],
cp
);
</script>

View File

@@ -0,0 +1,33 @@
<script src="../testing.js"></script>
<script id=default>
let event = new MouseEvent('click');
testing.expectEqual('click', event.type);
testing.expectEqual(true, event instanceof MouseEvent);
testing.expectEqual(true, event instanceof Event);
testing.expectEqual(0, event.clientX);
testing.expectEqual(0, event.clientY);
testing.expectEqual(0, event.screenX);
testing.expectEqual(0, event.screenY);
</script>
<script id=parameters>
let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20 });
testing.expectEqual(0, new_event.button);
testing.expectEqual(10, new_event.x);
testing.expectEqual(20, new_event.y);
testing.expectEqual(10, new_event.screenX);
testing.expectEqual(20, new_event.screenY);
</script>
<script id=listener>
let me = new MouseEvent('click');
testing.expectEqual(true, me instanceof Event);
var evt = null;
document.addEventListener('click', function (e) {
evt = e;
});
document.dispatchEvent(me);
testing.expectEqual('click', evt.type);
testing.expectEqual(true, evt instanceof MouseEvent);
</script>

175
src/tests/testing.js Normal file
View File

@@ -0,0 +1,175 @@
// Note: this code tries to make sure that we don't fail to execute a <script>
// block without reporting an error. In other words, if the test passes, you
// should be confident that the code actually ran.
// We do a couple things to ensure this.
// 1 - We make sure that ever script with an id has at least 1 assertion called
// 2 - We add an onerror handler to every script and, on error, fail.
//
// This is pretty straightforward, with the only complexity coming from "eventually"
// assertions, which are assertions we lazily check in `getStatus()`. We
// do this because, by the time `getStatus()`, `page.wait()` will have been called
// and any timer (setTimeout, requestAnimation, MutationObserver, etc...) will
// have been evaluated. Test which use/test these behavior will use `eventually`.
(() => {
function expectEqual(expected, actual) {
_recordExecution();
if (_equal(expected, actual)) {
return;
}
testing._status = 'fail';
let msg = `expected: ${JSON.stringify(expected)}, got: ${JSON.stringify(actual)}`;
console.warn(
`id: ${testing._captured?.script_id || document.currentScript.id}`,
`msg: ${msg}`,
`stack: ${testing._captured?.stack || new Error().stack}`,
);
}
function expectError(expected, fn) {
withError((err) => {
expectEqual(expected, err.toString());
}, fn);
}
function withError(cb, fn) {
try{
fn();
} catch (err) {
cb(err);
return;
}
expectEqual('an error', null);
}
// Should only be called by the test runner
function getStatus() {
// if we're already in a fail state, return fail, nothing can recover this
if (testing._status === 'fail') return 'fail';
// run any eventually's that we've captured
for (const ev of testing._eventually) {
testing._captured = ev[1];
ev[0]();
testing._captured = null;
}
// Again, if we're in a fail state, we can immediately fail
if (testing._status === 'fail') return 'fail';
// make sure that any <script id=xyz></script> tags we have have had at least
// 1 assertion. This helps ensure that if a script tag fails to execute,
// we'll report an error, even if no assertions failed.
const scripts = document.getElementsByTagName('script');
for (script of scripts) {
const id = script.id;
if (!id) {
continue;
}
if (!testing._executed_scripts[id]) {
console.warn(`Failed to execute any expectations for <script id="${id}">...</script>`),
testing._status = 'fail';
}
}
return testing._status;
}
// Set expectations to happen at some point in the future. Necessary for
// testing callbacks which will only be executed after page.wait is called.
function eventually(fn) {
// capture the current state (script id, stack) so that, when we do run this
// we can display more meaningful details on failure.
testing._eventually.push([fn, {
script_id: document.currentScript.id,
stack: new Error().stack,
}]);
_registerErrorCallback();
}
function _recordExecution() {
if (testing._status === 'fail') {
return;
}
testing._status = 'ok';
const script_id = testing._captured?.script_id || document.currentScript.id;
testing._executed_scripts[script_id] = true;
_registerErrorCallback();
}
// We want to attach an onError callback to each <script>, so that we can
// properly fail it.
function _registerErrorCallback() {
const script = document.currentScript;
if (!script) {
// can be null if we're executing an eventually assertion, but that's ok
// because the errorCallback would have been registered for this script
// already
return;
}
if (script.onerror) {
// already registered
return;
}
script.onerror = function(err, b) {
testing._status = 'fail';
console.warn(
`id: ${script.id}`,
`msg: There was an error executing the <script id=${script.id}>...</script>.\n There should be a eval error printed above this.`,
);
}
}
function _equal(a, b) {
if (a === b) {
return true;
}
if (a === null || b === null) {
return false;
}
if (typeof a !== 'object' || typeof b !== 'object') {
return false;
}
if (Object.keys(a).length != Object.keys(b).length) {
return false;
}
for (property in a) {
if (b.hasOwnProperty(property) === false) {
return false;
}
if (_equal(a[property], b[property]) === false) {
return false;
}
}
return true;
}
window.testing = {
_status: 'empty',
_eventually: [],
_executed_scripts: {},
_captured: null,
getStatus: getStatus,
eventually: eventually,
expectEqual: expectEqual,
expectError: expectError,
withError: withError,
};
// Helper, so you can do $(sel) in a test
window.$ = function(sel) {
return document.querySelector(sel);
}
// Helper, so you can do $$(sel) in a test
window.$$ = function(sel) {
return document.querySelectorAll(sel);
}
})();

View File

@@ -0,0 +1,12 @@
<script src="../testing.js"></script>
<body>
<iframe src="https://httpbin.io/html" title="iframea"></iframe>
<iframe src="https://httpbin.io/html" title="iframeb"></iframe>
</body>
<script id=frames>
testing.expectEqual(2, frames.length);
testing.expectEqual(undefined, frames[3])
testing.expectError('Error: TODO', () => { frames[1] });
</script>

View File

@@ -0,0 +1,105 @@
<script src="../testing.js"></script>
<body></body>
<script id=aliases>
testing.expectEqual(window, window.self);
testing.expectEqual(window, window.parent);
testing.expectEqual(window, window.top);
testing.expectEqual(window, window.frames);
testing.expectEqual(0, window.frames.length);
</script>
<script id=request_animation>
let start = 0;
function step(timestamp) {
start = timestamp;
}
requestAnimationFrame(step);
testing.eventually(() => testing.expectEqual(true, start > 0));
let request_id = requestAnimationFrame(() => {
start = 0;
});
cancelAnimationFrame(request_id);
testing.eventually(() => testing.expectEqual(true, start > 0));
</script>
<script id=dimensions>
testing.expectEqual(1, innerHeight);
// Width is 1 even if there are no elements
testing.expectEqual(1, innerWidth);
let div1 = document.createElement('div');
document.body.appendChild(div1);
div1.getClientRects()
let div2 = document.createElement('div');
document.body.appendChild(div2);
div2.getClientRects();
testing.expectEqual(1, innerHeight);
testing.expectEqual(2, innerWidth);
</script>
<script id=setTimeout>
let longCall = false;
window.setTimeout(() => {longCall = true}, 5001);
testing.eventually(() => testing.expectEqual(false, longCall));
let wst1 = 0;
window.setTimeout(() => {wst1 += 1}, 1);
testing.eventually(() => testing.expectEqual(1, wst1));
let wst2 = 1;
window.setTimeout((a, b) => {wst2 = a + b}, 1, 2, 3);
testing.eventually(() => testing.expectEqual(5, wst2));
</script>
<script id=eventTarget>
let called = false;
window.addEventListener("ready", (e) => {
called = (e.currentTarget == window);
}, {capture: false, once: false});
const evt = new Event("ready", { bubbles: true, cancelable: false });
window.dispatchEvent(evt);
testing.expectEqual(true, called);
</script>
<script id=btoa_atob>
const b64 = btoa('https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder')
testing.expectEqual('aHR0cHM6Ly96aWdsYW5nLm9yZy9kb2N1bWVudGF0aW9uL21hc3Rlci9zdGQvI3N0ZC5iYXNlNjQuQmFzZTY0RGVjb2Rlcg==', b64);
const str = atob(b64)
testing.expectEqual('https://ziglang.org/documentation/master/std/#std.base64.Base64Decoder', str);
testing.expectError('Error: InvalidCharacterError', () => {
atob('b');
});
</script>
<script id=scroll>
let scroll = false;
let scrolend = false
window.addEventListener('scroll', () => {scroll = true});
document.addEventListener('scrollend', () => {scrollend = true});
window.scrollTo(0);
testing.expectEqual(true, scroll);
testing.expectEqual(true, scrollend);
</script>
<script id=queueMicroTask>
var qm = false;
window.queueMicrotask(() => {qm = true });
testing.eventually(() => testing.expectEqual(true, qm));
</script>
<script id=DOMContentLoaded>
let dcl = false;
window.queueMicrotask(() => {qm = true });
window.addEventListener('DOMContentLoaded', (e) => {
dcl = e.target == document;
});
testing.eventually(() => testing.expectEqual(true, dcl));
</script>