From 81766c8517b5b9b95a1966fa84b3e4d175631ff8 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 2 Sep 2025 10:38:27 +0800 Subject: [PATCH] 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. --- src/browser/browser.zig | 8 +- src/browser/crypto/crypto.zig | 30 +--- src/browser/css/css.zig | 10 +- src/browser/encoding/TextDecoder.zig | 27 +--- src/browser/encoding/TextEncoder.zig | 18 +-- src/browser/events/custom_event.zig | 21 +-- src/browser/events/event.zig | 160 +-------------------- src/browser/events/mouse_event.zig | 32 +---- src/browser/html/window.zig | 4 +- src/main.zig | 2 +- src/testing.zig | 4 +- src/tests/browser.html | 5 + src/tests/crypto.html | 25 ++++ src/tests/css.html | 5 + src/tests/encoding/decoder.html | 17 +++ src/tests/encoding/encoder.html | 13 ++ src/tests/events/custom.html | 16 +++ src/tests/events/event.html | 138 ++++++++++++++++++ src/tests/events/mouse.html | 33 +++++ src/{browser => }/tests/testing.js | 49 +++++-- src/{browser => }/tests/window/frames.html | 0 src/{browser => }/tests/window/window.html | 0 22 files changed, 317 insertions(+), 300 deletions(-) create mode 100644 src/tests/browser.html create mode 100644 src/tests/crypto.html create mode 100644 src/tests/css.html create mode 100644 src/tests/encoding/decoder.html create mode 100644 src/tests/encoding/encoder.html create mode 100644 src/tests/events/custom.html create mode 100644 src/tests/events/event.html create mode 100644 src/tests/events/mouse.html rename src/{browser => }/tests/testing.js (80%) rename src/{browser => }/tests/window/frames.html (100%) rename src/{browser => }/tests/window/window.html (100%) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 26687c53..09ceef1c 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -112,11 +112,5 @@ pub const Browser = struct { const testing = @import("../testing.zig"); test "Browser" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{}); - defer runner.deinit(); - - // this will crash if ICU isn't properly configured / ininitialized - try runner.testCases(&.{ - .{ "new Intl.DateTimeFormat()", "[object Intl.DateTimeFormat]" }, - }, .{}); + try testing.htmlRunner("browser.html"); } diff --git a/src/browser/crypto/crypto.zig b/src/browser/crypto/crypto.zig index 20cfb4e2..27813b4b 100644 --- a/src/browser/crypto/crypto.zig +++ b/src/browser/crypto/crypto.zig @@ -66,32 +66,6 @@ const RandomValues = union(enum) { }; const testing = @import("../../testing.zig"); -test "Browser.Crypto" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{}); - defer runner.deinit(); - - try runner.testCases(&.{ - .{ "const a = crypto.randomUUID();", "undefined" }, - .{ "const b = crypto.randomUUID();", "undefined" }, - .{ "a.length;", "36" }, - .{ "b.length;", "36" }, - .{ "a == b;", "false" }, - }, .{}); - - try runner.testCases(&.{ - .{ "try { crypto.getRandomValues(new BigUint64Array(8193)) } catch(e) { e.message == 'QuotaExceededError' }", "true" }, - .{ "let r1 = new Int32Array(5)", "undefined" }, - .{ "let r2 = crypto.getRandomValues(r1)", "undefined" }, - .{ "new Set(r1).size", "5" }, - .{ "new Set(r2).size", "5" }, - .{ "r1.every((v, i) => v === r2[i])", "true" }, - }, .{}); - - try runner.testCases(&.{ - .{ "var r3 = new Uint8Array(16)", null }, - .{ "let r4 = crypto.getRandomValues(r3)", "undefined" }, - .{ "r4[6] = 10", null }, - .{ "r4[6]", "10" }, - .{ "r3[6]", "10" }, - }, .{}); +test "Browser: Crypto" { + try testing.htmlRunner("crypto.html"); } diff --git a/src/browser/css/css.zig b/src/browser/css/css.zig index 72ceb8e9..01fad349 100644 --- a/src/browser/css/css.zig +++ b/src/browser/css/css.zig @@ -190,12 +190,6 @@ test "parse" { } const testing = @import("../../testing.zig"); -test "Browser.HTML.CSS" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{}); - defer runner.deinit(); - - try runner.testCases(&.{ - .{ "CSS.supports('display: flex')", "true" }, - .{ "CSS.supports('text-decoration-style', 'blink')", "true" }, - }, .{}); +test "Browser: CSS" { + try testing.htmlRunner("css.html"); } diff --git a/src/browser/encoding/TextDecoder.zig b/src/browser/encoding/TextDecoder.zig index 99ea06e2..d0718de7 100644 --- a/src/browser/encoding/TextDecoder.zig +++ b/src/browser/encoding/TextDecoder.zig @@ -79,29 +79,6 @@ pub fn _decode(self: *const TextDecoder, v: []const u8) ![]const u8 { } const testing = @import("../../testing.zig"); -test "Browser.Encoding.TextDecoder" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{ - .html = "", - }); - defer runner.deinit(); - - try runner.testCases(&.{ - .{ "let d1 = new TextDecoder();", null }, - .{ "d1.encoding;", "utf-8" }, - .{ "d1.fatal", "false" }, - .{ "d1.ignoreBOM", "false" }, - .{ "d1.decode(new Uint8Array([240, 160, 174, 183]))", "𠮷" }, - .{ "d1.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 240, 160, 174, 183]))", "𠮷" }, - .{ "d1.decode(new Uint8Array([49, 50]).buffer)", "12" }, - - .{ "let d2 = new TextDecoder('utf8', {fatal: true})", null }, - .{ - \\ try { - \\ let data = new Uint8Array([240, 240, 160, 174, 183]); - \\ d2.decode(data); - \\ } catch (e) {e} - , - "Error: InvalidUtf8", - }, - }, .{}); +test "Browser: Encoding.TextDecoder" { + try testing.htmlRunner("encoding/decoder.html"); } diff --git a/src/browser/encoding/TextEncoder.zig b/src/browser/encoding/TextEncoder.zig index 600d5b82..a1551e81 100644 --- a/src/browser/encoding/TextEncoder.zig +++ b/src/browser/encoding/TextEncoder.zig @@ -43,20 +43,6 @@ pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) { } const testing = @import("../../testing.zig"); -test "Browser.Encoding.TextEncoder" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{ - .html = "", - }); - defer runner.deinit(); - - try runner.testCases(&.{ - .{ "var encoder = new TextEncoder();", null }, - .{ "encoder.encoding;", "utf-8" }, - .{ "encoder.encode('€');", "226,130,172" }, - - // Invalid utf-8 sequence. - // Result with chrome: - // .{ "encoder.encode(new Uint8Array([0xE2,0x28,0xA1]))", "50,50,54,44,52,48,44,49,54,49" }, - .{ "try {encoder.encode(new Uint8Array([0xE2,0x28,0xA1])) } catch (e) { e };", "Error: InvalidUtf8" }, - }, .{}); +test "Browser: Encoding.TextEncoder" { + try testing.htmlRunner("encoding/encoder.html"); } diff --git a/src/browser/events/custom_event.zig b/src/browser/events/custom_event.zig index d5ce0bd3..3bf8179f 100644 --- a/src/browser/events/custom_event.zig +++ b/src/browser/events/custom_event.zig @@ -58,23 +58,6 @@ pub const CustomEvent = struct { }; const testing = @import("../../testing.zig"); -test "Browser.CustomEvent" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{}); - defer runner.deinit(); - - try runner.testCases(&.{ - .{ "let capture = null", "undefined" }, - .{ "const el = document.createElement('div');", "undefined" }, - .{ "el.addEventListener('c1', (e) => { capture = 'c1-' + new String(e.detail)})", "undefined" }, - .{ "el.addEventListener('c2', (e) => { capture = 'c2-' + new String(e.detail.over)})", "undefined" }, - - .{ "el.dispatchEvent(new CustomEvent('c1'));", "true" }, - .{ "capture", "c1-null" }, - - .{ "el.dispatchEvent(new CustomEvent('c1', {detail: '123'}));", "true" }, - .{ "capture", "c1-123" }, - - .{ "el.dispatchEvent(new CustomEvent('c2', {detail: {over: 9000}}));", "true" }, - .{ "capture", "c2-9000" }, - }, .{}); +test "Browser: Events.Custom" { + try testing.htmlRunner("events/custom.html"); } diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig index 11ae9f4c..f11cf07d 100644 --- a/src/browser/events/event.zig +++ b/src/browser/events/event.zig @@ -110,8 +110,11 @@ pub const Event = struct { return try parser.eventIsTrusted(self); } - pub fn get_timestamp(self: *parser.Event) !u32 { - return try parser.eventTimestamp(self); + // Even though this is supposed to to provide microsecond resolution, browser + // return coarser values to protect against fingerprinting. libdom returns + // seconds, which is good enough. + pub fn get_timeStamp(self: *parser.Event) !u32 { + return parser.eventTimestamp(self); } // Methods @@ -386,155 +389,6 @@ const SignalCallback = struct { }; const testing = @import("../../testing.zig"); -test "Browser.Event" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{}); - defer runner.deinit(); - - try runner.testCases(&.{ - .{ "let content = document.getElementById('content')", "undefined" }, - .{ "let para = document.getElementById('para')", "undefined" }, - .{ "var nb = 0; var evt", "undefined" }, - }, .{}); - - try runner.testCases(&.{ - .{ - \\ content.addEventListener('target', function(e) { - \\ evt = e; nb = nb + 1; - \\ e.preventDefault(); - \\ }) - , - "undefined", - }, - .{ "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", "false" }, - .{ "nb", "1" }, - .{ "evt.target === content", "true" }, - .{ "evt.bubbles", "true" }, - .{ "evt.cancelable", "true" }, - .{ "evt.defaultPrevented", "true" }, - .{ "evt.isTrusted", "true" }, - .{ "evt.timestamp > 1704063600", "true" }, // 2024/01/01 00:00 - // event.type, event.currentTarget, event.phase checked in EventTarget - }, .{}); - - try runner.testCases(&.{ - .{ "nb = 0", "0" }, - .{ - \\ content.addEventListener('stop',function(e) { - \\ e.stopPropagation(); - \\ nb = nb + 1; - \\ }, true) - , - "undefined", - }, - // the following event listener will not be invoked - .{ - \\ para.addEventListener('stop',function(e) { - \\ nb = nb + 1; - \\ }) - , - "undefined", - }, - .{ "para.dispatchEvent(new Event('stop'))", "true" }, - .{ "nb", "1" }, // will be 2 if event was not stopped at content event listener - }, .{}); - - try runner.testCases(&.{ - .{ "nb = 0", "0" }, - .{ - \\ content.addEventListener('immediate', function(e) { - \\ e.stopImmediatePropagation(); - \\ nb = nb + 1; - \\ }) - , - "undefined", - }, - // the following event listener will not be invoked - .{ - \\ content.addEventListener('immediate', function(e) { - \\ nb = nb + 1; - \\ }) - , - "undefined", - }, - .{ "content.dispatchEvent(new Event('immediate'))", "true" }, - .{ "nb", "1" }, // will be 2 if event was not stopped at first content event listener - }, .{}); - - try runner.testCases(&.{ - .{ "nb = 0", "0" }, - .{ - \\ content.addEventListener('legacy', function(e) { - \\ evt = e; nb = nb + 1; - \\ }) - , - "undefined", - }, - .{ "let evtLegacy = document.createEvent('Event')", "undefined" }, - .{ "evtLegacy.initEvent('legacy')", "undefined" }, - .{ "content.dispatchEvent(evtLegacy)", "true" }, - .{ "nb", "1" }, - }, .{}); - - try runner.testCases(&.{ - .{ "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", "undefined" }, - .{ "document.addEventListener('count', cbk)", "undefined" }, - .{ "document.removeEventListener('count', cbk)", "undefined" }, - .{ "document.dispatchEvent(new Event('count'))", "true" }, - .{ "nb", "0" }, - }, .{}); - - try runner.testCases(&.{ - .{ "nb = 0; function cbk(event) { nb ++; }", null }, - .{ "document.addEventListener('count', cbk, {once: true})", null }, - .{ "document.dispatchEvent(new Event('count'))", "true" }, - .{ "document.dispatchEvent(new Event('count'))", "true" }, - .{ "document.dispatchEvent(new Event('count'))", "true" }, - .{ "nb", "1" }, - .{ "document.removeEventListener('count', cbk)", "undefined" }, - }, .{}); - - try runner.testCases(&.{ - .{ "nb = 0; function cbk(event) { nb ++; }", null }, - .{ "let ac = new AbortController()", null }, - .{ "document.addEventListener('count', cbk, {signal: ac.signal})", null }, - .{ "document.dispatchEvent(new Event('count'))", "true" }, - .{ "document.dispatchEvent(new Event('count'))", "true" }, - .{ "ac.abort()", null }, - .{ "document.dispatchEvent(new Event('count'))", "true" }, - .{ "nb", "2" }, - .{ "document.removeEventListener('count', cbk)", "undefined" }, - }, .{}); - - try runner.testCases(&.{ - .{ "new Event('').composedPath()", "" }, - .{ - \\ let div1 = document.createElement('div'); - \\ let sr1 = div1.attachShadow({mode: 'open'}); - \\ sr1.innerHTML = "

"; - \\ document.getElementsByTagName('body')[0].appendChild(div1); - \\ let cp = null; - \\ div1.addEventListener('click', (e) => { - \\ cp = e.composedPath().map((n) => n.id || n.nodeName || n.toString()); - \\ }); - \\ sr1.getElementById('srp1').click(); - \\ cp.join(', '); - , - "srp1, #document-fragment, DIV, BODY, HTML, #document, [object Window]", - }, - - .{ - \\ let div2 = document.createElement('div'); - \\ let sr2 = div2.attachShadow({mode: 'closed'}); - \\ sr2.innerHTML = "

"; - \\ document.getElementsByTagName('body')[0].appendChild(div2); - \\ cp = null; - \\ div2.addEventListener('click', (e) => { - \\ cp = e.composedPath().map((n) => n.id || n.nodeName || n.toString()); - \\ }); - \\ sr2.getElementById('srp2').click(); - \\ cp.join(', '); - , - "DIV, BODY, HTML, #document, [object Window]", - }, - }, .{}); +test "Browser: Event" { + try testing.htmlRunner("events/event.html"); } diff --git a/src/browser/events/mouse_event.zig b/src/browser/events/mouse_event.zig index 0eeecda0..b95f3993 100644 --- a/src/browser/events/mouse_event.zig +++ b/src/browser/events/mouse_event.zig @@ -107,34 +107,6 @@ pub const MouseEvent = struct { }; const testing = @import("../../testing.zig"); -test "Browser.MouseEvent" { - var runner = try testing.jsRunner(testing.tracking_allocator, .{}); - defer runner.deinit(); - - try runner.testCases(&.{ - // Default MouseEvent - .{ "let event = new MouseEvent('click')", "undefined" }, - .{ "event.type", "click" }, - .{ "event instanceof MouseEvent", "true" }, - .{ "event instanceof Event", "true" }, - .{ "event.clientX", "0" }, - .{ "event.clientY", "0" }, - .{ "event.screenX", "0" }, - .{ "event.screenY", "0" }, - // MouseEvent with parameters - .{ "let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20 })", "undefined" }, - .{ "new_event.button", "0" }, - .{ "new_event.x", "10" }, - .{ "new_event.y", "20" }, - .{ "new_event.screenX", "10" }, - .{ "new_event.screenY", "20" }, - // MouseEvent Listener - .{ "let me = new MouseEvent('click')", "undefined" }, - .{ "me instanceof Event", "true" }, - .{ "var eevt = null; function ccbk(event) { eevt = event; }", "undefined" }, - .{ "document.addEventListener('click', ccbk)", "undefined" }, - .{ "document.dispatchEvent(me)", "true" }, - .{ "eevt.type", "click" }, - .{ "eevt instanceof MouseEvent", "true" }, - }, .{}); +test "Browser: Events.Mouse" { + try testing.htmlRunner("events/mouse.html"); } diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index b88d7016..37a1dda2 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -439,6 +439,6 @@ const TimerCallback = struct { const testing = @import("../../testing.zig"); test "Browser: Window" { - try testing.newRunner("window/window.html"); - try testing.newRunner("window/frames.html"); + try testing.htmlRunner("window/window.html"); + try testing.htmlRunner("window/frames.html"); } diff --git a/src/main.zig b/src/main.zig index ab88dede..916b3c9f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -796,7 +796,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { }); } - if (std.mem.startsWith(u8, path, "/src/browser/tests/")) { + if (std.mem.startsWith(u8, path, "/src/tests/")) { // strip off leading / so that it's relative to CWD return TestHTTPServer.sendFile(req, path[1..]); } diff --git a/src/testing.zig b/src/testing.zig index 200e2ce7..7bc430af 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -506,7 +506,7 @@ pub fn shutdown() void { test_app.deinit(); } -pub fn newRunner(file: []const u8) !void { +pub fn htmlRunner(file: []const u8) !void { defer _ = arena_instance.reset(.retain_capacity); const page = try test_session.createPage(); defer test_session.removePage(); @@ -516,7 +516,7 @@ pub fn newRunner(file: []const u8) !void { try_catch.init(js_context); defer try_catch.deinit(); - const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/browser/tests/{s}", .{file}); + const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file}); try page.navigate(url, .{}); page.wait(2); diff --git a/src/tests/browser.html b/src/tests/browser.html new file mode 100644 index 00000000..d80130a4 --- /dev/null +++ b/src/tests/browser.html @@ -0,0 +1,5 @@ + + diff --git a/src/tests/crypto.html b/src/tests/crypto.html new file mode 100644 index 00000000..46a5c221 --- /dev/null +++ b/src/tests/crypto.html @@ -0,0 +1,25 @@ + + diff --git a/src/tests/css.html b/src/tests/css.html new file mode 100644 index 00000000..51f69748 --- /dev/null +++ b/src/tests/css.html @@ -0,0 +1,5 @@ + + diff --git a/src/tests/encoding/decoder.html b/src/tests/encoding/decoder.html new file mode 100644 index 00000000..822fdb69 --- /dev/null +++ b/src/tests/encoding/decoder.html @@ -0,0 +1,17 @@ + + diff --git a/src/tests/encoding/encoder.html b/src/tests/encoding/encoder.html new file mode 100644 index 00000000..bc394d6a --- /dev/null +++ b/src/tests/encoding/encoder.html @@ -0,0 +1,13 @@ + + diff --git a/src/tests/events/custom.html b/src/tests/events/custom.html new file mode 100644 index 00000000..e749fe05 --- /dev/null +++ b/src/tests/events/custom.html @@ -0,0 +1,16 @@ + + diff --git a/src/tests/events/event.html b/src/tests/events/event.html new file mode 100644 index 00000000..f6d0374a --- /dev/null +++ b/src/tests/events/event.html @@ -0,0 +1,138 @@ + + +
+

+
+ + + + + + + + + + + + + + + + diff --git a/src/tests/events/mouse.html b/src/tests/events/mouse.html new file mode 100644 index 00000000..ba6c47ba --- /dev/null +++ b/src/tests/events/mouse.html @@ -0,0 +1,33 @@ + + + + + + diff --git a/src/browser/tests/testing.js b/src/tests/testing.js similarity index 80% rename from src/browser/tests/testing.js rename to src/tests/testing.js index 2befecbf..30d6306a 100644 --- a/src/browser/tests/testing.js +++ b/src/tests/testing.js @@ -13,16 +13,17 @@ (() => { function expectEqual(expected, actual) { _recordExecution(); - if (expected !== actual) { - 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}`, - ); + 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) { @@ -41,6 +42,7 @@ 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'; @@ -97,6 +99,8 @@ _registerErrorCallback(); } + // We want to attach an onError callback to each