Files
2026-07-13 12:29:49 +08:00

1168 lines
52 KiB
Zig

//! End-to-end proof battery for examples/soundboard-ts — the launch-gate
//! port: a soundboard-complexity app authored in TypeScript + Native
//! markup with ZERO hand-written Zig. The build transpiles the example's
//! REAL core (examples/soundboard-ts/src/core.ts) and this suite drives
//! it through `TsUiApp` with the example's SHIPPING markup
//! (app.native, staged beside this file), so every pin here is the
//! product path, not a fixture:
//!
//! - the markup view binds the ported catalog (grid, counts, the
//! now-playing bar's idle state);
//! - playback flows end to end: play -> the engine's audio request
//! (prepared path, hosted URL, manifest byte size) -> loaded ->
//! position ticks -> the rendered clock -> auto-advance at the
//! natural end, with the play-next queue winning the advance;
//! - the duration rule: the platform's estimate never replaces the
//! manifest total;
//! - the one-channel stale-event window: position/completed reports
//! from a replaced playback are dropped by the model guard;
//! - the rendered-clock subscription arms a REAL platform timer only
//! while audio moves;
//! - Copy Title lands on the clipboard channel;
//! - search drives the full byte-splice text engine from raw platform
//! text input;
//! - a recorded session (user input + scripted audio events) replays
//! byte-identically with zero host calls;
//! - dispatch stays far under the frame budget at the rendered-clock
//! cadence (the "on_frame cadence untested" flag, measured).
//!
//! Only this TEST wiring is Zig — the command mapper below exists so the
//! suite can dispatch payload-carrying Msgs through the journaled
//! menu-command path (the app itself dispatches them from markup).
const std = @import("std");
const builtin = @import("builtin");
const native_sdk = @import("native_sdk");
const core = @import("ts_soundboard_core");
const runtime_ns = native_sdk.runtime;
const canvas = native_sdk.canvas;
const geometry = native_sdk.geometry;
const Adapter = native_sdk.TsUiApp(core);
const App = Adapter.App;
const Bridge = Adapter.Host;
const app_markup = @embedFile("app.native");
const CompiledAppView = canvas.CompiledMarkupView(core.Model, core.Msg, app_markup);
const canvas_label = "soundboard-canvas";
/// The playclock subscription's platform timer id: the bridge's timer
/// slot 0 lands in engine timer slot 0.
const playclock_platform_id: u64 = runtime_ns.effect_timer_platform_id_base + 0;
const app_views = [_]native_sdk.ShellView{
.{ .label = canvas_label, .kind = .gpu_surface, .fill = true, .gpu_backend = .metal },
};
const app_windows = [_]native_sdk.ShellWindow{.{
.label = "main",
.title = "Soundboard TS",
.width = 1080,
.height = 720,
.views = &app_views,
}};
const app_scene: native_sdk.ShellConfig = .{ .windows = &app_windows };
/// TEST-ONLY command mapper: lets the suite dispatch the payload-carrying
/// Msgs the app's markup dispatches from presses and context menus, but
/// through the journaled menu-command path (record/replay needs every
/// input in the journal). "sb.play.12" plays track 12, etc.
fn testCommand(name: []const u8) ?core.Msg {
if (std.mem.eql(u8, name, "sb.toggle")) return .toggle_play;
if (std.mem.eql(u8, name, "sb.next")) return .next_track;
if (std.mem.eql(u8, name, "sb.prev")) return .prev_track;
if (std.mem.eql(u8, name, "sb.songs")) return .show_songs;
if (std.mem.eql(u8, name, "sb.albums")) return .show_albums;
if (commandId(name, "sb.play.")) |id| return .{ .play_track = id };
if (commandId(name, "sb.playalbum.")) |id| return .{ .play_album = id };
if (commandId(name, "sb.open.")) |id| return .{ .open_album = id };
if (commandId(name, "sb.queue.")) |id| return .{ .queue_track = id };
if (commandId(name, "sb.copy.")) |id| return .{ .copy_title = id };
return null;
}
fn commandId(name: []const u8, prefix: []const u8) ?i64 {
if (!std.mem.startsWith(u8, name, prefix)) return null;
return std.fmt.parseInt(i64, name[prefix.len..], 10) catch null;
}
fn appOptions() App.Options {
return .{
.name = "soundboard-ts-e2e",
.scene = app_scene,
.canvas_label = canvas_label,
// The comptime-compiled engine over the example's shipping markup
// — the whole view tier of the app under test.
.view = CompiledAppView.build,
.on_command = testCommand,
// The manifest theme channel, mirrored from the example's
// app.zon exactly as the generated wiring resolves it
// (manifestThemePack + manifestThemeAccent): the geist pack
// under the Zig original's pink accent identity.
.theme = .geist,
.theme_accent = canvas.Color.rgb8(0xdf, 0x26, 0x70),
};
}
const Harness = struct {
harness: *native_sdk.TestHarness(),
app_state: *App,
app: native_sdk.App,
clock: native_sdk.TestClock,
fn create() !*Harness {
return createFull(null, .real);
}
/// A harness on the FAKE effects executor: clipboard requests park in
/// fake slots for assertions instead of reaching platform services.
fn createFake() !*Harness {
return createFull(null, .fake);
}
fn createRecorded(recorder: ?*runtime_ns.SessionRecorder) !*Harness {
return createFull(recorder, .real);
}
fn createFull(recorder: ?*runtime_ns.SessionRecorder, executor: runtime_ns.EffectExecutor) !*Harness {
return createConfigured(recorder, executor, .{});
}
/// A harness with adapter-owned knobs (boot images, env overrides) —
/// the CoreOptions the generated wiring passes from app.zon assets
/// and the launch environment.
fn createConfigured(recorder: ?*runtime_ns.SessionRecorder, executor: runtime_ns.EffectExecutor, core_options: Adapter.CoreOptions) !*Harness {
const self = try std.testing.allocator.create(Harness);
errdefer std.testing.allocator.destroy(self);
self.clock = .{};
self.clock.setWallMs(60_000);
self.harness = try native_sdk.TestHarness().create(std.testing.allocator, .{
.size = geometry.SizeF.init(1080, 720),
});
errdefer self.harness.destroy(std.testing.allocator);
self.harness.null_platform.gpu_surfaces = true;
self.harness.null_platform.image_decode = true;
self.harness.runtime.options.session_recorder = recorder;
self.app_state = try std.testing.allocator.create(App);
errdefer std.testing.allocator.destroy(self.app_state);
self.app_state.* = Adapter.init(std.heap.page_allocator, core_options, appOptions());
self.app_state.effects.executor = executor;
self.app_state.effects.clock = self.clock.clock();
self.app = self.app_state.app();
try self.harness.start(self.app);
try self.harness.runtime.dispatchPlatformEvent(self.app, .{ .gpu_surface_frame = .{
.label = canvas_label,
.size = geometry.SizeF.init(1080, 720),
.scale_factor = 1,
.frame_index = 1,
.timestamp_ns = 1_000_000,
} });
try std.testing.expect(self.app_state.installed);
return self;
}
fn destroy(self: *Harness) void {
self.app_state.deinit();
std.testing.allocator.destroy(self.app_state);
self.harness.destroy(std.testing.allocator);
std.testing.allocator.destroy(self);
}
fn wake(self: *Harness) !void {
try self.harness.runtime.dispatchPlatformEvent(self.app, .wake);
}
/// Feed one scripted audio event and drain it into the core (the
/// engine parks events until a dispatchable wake).
fn audio(self: *Harness, kind: anytype, position_ms: u64, duration_ms: u64, playing: bool) !void {
try self.app_state.effects.feedAudioEvent(kind, position_ms, duration_ms, playing);
try self.wake();
}
fn audioBuffering(self: *Harness, kind: anytype, position_ms: u64, duration_ms: u64, playing: bool, buffering: bool) !void {
try self.app_state.effects.feedAudioEventBuffering(kind, position_ms, duration_ms, playing, buffering);
try self.wake();
}
fn menu(self: *Harness, name: []const u8) !void {
try self.harness.runtime.dispatchPlatformEvent(self.app, .{ .menu_command = .{ .name = name, .window_id = 1 } });
}
fn hasText(self: *Harness, text: []const u8) bool {
return findTextIn(self.app_state.tree.?.root, text);
}
fn findId(self: *Harness, kind: canvas.WidgetKind, text: []const u8) ?canvas.ObjectId {
return findKindText(self.app_state.tree.?.root, kind, text);
}
fn findLabel(self: *Harness, label: []const u8) ?canvas.ObjectId {
return findByLabel(self.app_state.tree.?.root, label);
}
/// Click a rendered widget through the automation verb — the same
/// headless path `native automate` drives.
fn click(self: *Harness, id: canvas.ObjectId) !void {
var buffer: [96]u8 = undefined;
const command = try std.fmt.bufPrint(&buffer, "widget-click {s} {d}", .{ canvas_label, id });
try self.harness.runtime.dispatchAutomationCommand(self.app, command);
}
fn textInput(self: *Harness, text: []const u8) !void {
try self.harness.runtime.dispatchPlatformEvent(self.app, .{ .gpu_surface_input = .{
.window_id = 1,
.label = canvas_label,
.kind = .text_input,
.text = text,
} });
}
fn keyDown(self: *Harness, key: []const u8) !void {
try self.harness.runtime.dispatchPlatformEvent(self.app, .{ .gpu_surface_input = .{
.window_id = 1,
.label = canvas_label,
.kind = .key_down,
.key = key,
} });
}
fn firePlayclock(self: *Harness, timestamp_ns: u64) !bool {
const event = self.harness.null_platform.fireTimer(playclock_platform_id, timestamp_ns) orelse return false;
try self.harness.runtime.dispatchPlatformEvent(self.app, event);
return true;
}
fn playclockArmed(self: *Harness) bool {
const timer = self.harness.null_platform.startedTimer(playclock_platform_id) orelse return false;
return timer.active;
}
};
fn findKindText(widget: canvas.Widget, kind: canvas.WidgetKind, text: []const u8) ?canvas.ObjectId {
if (widget.kind == kind and std.mem.eql(u8, widget.text, text)) return widget.id;
for (widget.children) |child| {
if (findKindText(child, kind, text)) |id| return id;
}
return null;
}
fn findTextIn(widget: canvas.Widget, text: []const u8) bool {
if (std.mem.indexOf(u8, widget.text, text) != null) return true;
for (widget.children) |child| {
if (findTextIn(child, text)) return true;
}
return false;
}
fn findByLabel(widget: canvas.Widget, label: []const u8) ?canvas.ObjectId {
if (std.mem.eql(u8, widget.semantics.label, label)) return widget.id;
for (widget.children) |child| {
if (findByLabel(child, label)) |id| return id;
}
return null;
}
// -------------------------------------------------------- markup binding
test "the shipping markup binds the ported catalog: grid, counts, and the idle bar" {
const h = try Harness.create();
defer h.destroy();
// The album grid page with the full committed catalog.
try std.testing.expect(h.hasText("8 of 8"));
try std.testing.expect(h.hasText("Exit Signs"));
try std.testing.expect(h.hasText("Channel Surfing"));
try std.testing.expect(h.hasText("Harbor Sleep"));
try std.testing.expect(!h.hasText("Playing"));
// The idle now-playing bar: prompt title, placeholder clocks.
try std.testing.expect(h.hasText("Nothing playing"));
try std.testing.expect(h.hasText("Pick an album or a song to start"));
try std.testing.expect(h.hasText("-:--"));
// The Songs page mounts every catalog track as a markup row with the
// artist-album subtitle (the byte-built em-dash join).
try h.menu("sb.songs");
try std.testing.expect(h.hasText("68 of 68"));
try std.testing.expect(h.hasText("Mile Marker West"));
try std.testing.expect(h.hasText("Harbor Sleep — Exit Signs"));
// A markup album tile press opens the detail page: heading, the
// byte-built meta line, and the record's rows (numbers, durations).
try h.menu("sb.albums");
try h.click(h.findLabel("Exit Signs").?);
try std.testing.expect(h.hasText("Harbor Sleep · 2025 · 8 tracks"));
try std.testing.expect(h.hasText("Winter Birds"));
try std.testing.expect(h.hasText("2:44")); // 164832ms, the manifest duration
try std.testing.expect(h.findId(.button, "Play album") != null);
// Back to the grid through the markup back button.
try h.click(h.findId(.button, "Back to albums").?);
try std.testing.expect(h.hasText("8 of 8"));
}
test "the runtime markup interpreter builds the emitted model exactly like the compiled engine" {
// The PRODUCT wiring runs app.native through the runtime interpreter
// (hot reload); this suite compiles it at comptime. Hold the two
// engines text-identical over the booted model so the product path
// can never drift from the tested one.
const h = try Harness.create();
defer h.destroy();
var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_state.deinit();
const arena = arena_state.allocator();
const model = h.app_state.model;
const AppUi = canvas.Ui(core.Msg);
var interpreter_view = try canvas.MarkupView(core.Model, core.Msg).init(arena, app_markup);
var interpreter_ui = AppUi.init(arena);
const interpreted = try interpreter_ui.finalize(try interpreter_view.build(&interpreter_ui, &model));
var compiled_ui = AppUi.init(arena);
const compiled = try compiled_ui.finalize(CompiledAppView.build(&compiled_ui, &model));
var interpreted_texts: std.ArrayListUnmanaged(u8) = .empty;
defer interpreted_texts.deinit(std.testing.allocator);
var compiled_texts: std.ArrayListUnmanaged(u8) = .empty;
defer compiled_texts.deinit(std.testing.allocator);
try collectTexts(interpreted.root, &interpreted_texts, std.testing.allocator);
try collectTexts(compiled.root, &compiled_texts, std.testing.allocator);
try std.testing.expectEqualStrings(interpreted_texts.items, compiled_texts.items);
try std.testing.expect(std.mem.indexOf(u8, compiled_texts.items, "Nothing playing") != null);
try std.testing.expect(std.mem.indexOf(u8, compiled_texts.items, "Exit Signs") != null);
}
fn collectTexts(widget: canvas.Widget, out: *std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !void {
try out.appendSlice(allocator, widget.text);
try out.append(allocator, '\n');
for (widget.children) |child| {
try collectTexts(child, out, allocator);
}
}
// ------------------------------------------------------------- playback
test "playback flows end to end: play, the duration rule, the rendered clock, auto-advance, and the queue" {
const h = try Harness.create();
defer h.destroy();
const fx = &h.app_state.effects;
// A markup row press starts REAL playback: the engine channel holds
// the request whole — the prepared local path first, the hosted URL
// fallback, and the manifest's byte size as the cache integrity gate.
try h.click(h.findLabel("Exit Signs").?);
try h.click(h.findId(.button, "Play album").?);
const request = fx.pendingAudio().?;
try std.testing.expectEqual(runtime_ns.ts_core_audio_key_base, request.key);
try std.testing.expectEqualStrings("assets/music/exit-signs/mile-marker-west.mp3", request.path);
try std.testing.expectEqualStrings("https://xksenynjs1imkkii.public.blob.vercel-storage.com/music/exit-signs/mile-marker-west.mp3", request.url);
try std.testing.expectEqual(@as(u64, 3982923), request.expected_bytes);
try std.testing.expect(Bridge.model().playing);
try std.testing.expect(Bridge.model().loadPending);
// The duration rule: the platform's own estimate (165s) never
// replaces the manifest total (164832ms renders as 2:44 in the bar
// AND the track list — the two surfaces agree by construction).
try h.audio(.loaded, 0, 165_000, true);
try std.testing.expect(!Bridge.model().loadPending);
try std.testing.expectEqual(@as(i64, 164_832), Bridge.model().nowDurationMs);
try std.testing.expectEqual(@as(i64, 165_000), Bridge.model().platformDurationMs);
try std.testing.expect(h.hasText("2:44"));
try std.testing.expect(h.hasText("0:00"));
// Position ticks move the rendered clock; the elapsed label follows.
try h.audio(.position, 1_500, 165_000, true);
try std.testing.expectEqual(@as(i64, 1_500), Bridge.model().elapsedMs);
try std.testing.expect(h.hasText("0:01"));
// The never-rewind rule: a small backward tick holds flat while the
// clock is in motion; a past-slack disagreement snaps.
try h.audio(.position, 1_400, 165_000, true);
try std.testing.expectEqual(@as(i64, 1_500), Bridge.model().elapsedMs);
try h.audio(.position, 100, 165_000, true);
try std.testing.expectEqual(@as(i64, 100), Bridge.model().elapsedMs);
// The rendered-clock subscription is a REAL platform timer that
// exists exactly while audio moves; each fire advances one interval.
try std.testing.expect(h.playclockArmed());
try std.testing.expect(try h.firePlayclock(2_000_000));
try std.testing.expectEqual(@as(i64, 350), Bridge.model().elapsedMs);
// The honest buffering state reads where the artist normally does,
// and the motion-gated subscription reconciles away.
try h.audioBuffering(.position, 400, 165_000, true, true);
try std.testing.expect(Bridge.model().buffering);
try std.testing.expect(h.hasText("buffering..."));
try std.testing.expect(!h.playclockArmed());
try h.audioBuffering(.position, 900, 165_000, true, false);
try std.testing.expect(!Bridge.model().buffering);
try std.testing.expect(h.playclockArmed());
// Queue a track from the songs library (the markup dispatches this
// from the row's context menu): the badge appears and the natural
// end advances into the QUEUE, not the album order.
try h.menu("sb.queue.16");
try std.testing.expect(h.hasText("1 queued"));
// The queued track's row wears the Up-next badge (it lives in the
// songs library - a different album than the open detail page).
try h.menu("sb.songs");
try std.testing.expect(h.hasText("Up next"));
try h.menu("sb.albums");
try h.audio(.completed, 164_832, 165_000, false);
const queued_request = fx.pendingAudio().?;
try std.testing.expectEqualStrings("assets/music/second-nature/velvet-jackpot.mp3", queued_request.path);
try std.testing.expectEqual(@as(?i64, 16), Bridge.model().now);
try std.testing.expectEqual(@as(usize, 0), Bridge.model().queue.len);
try std.testing.expect(!h.hasText("1 queued"));
// With the queue drained, completion advances through the album in
// manifest order (Velvet Jackpot -> Second Nature).
try h.audio(.loaded, 0, 131_000, true);
try h.audio(.completed, 130_872, 131_000, false);
try std.testing.expectEqual(@as(?i64, 17), Bridge.model().now);
try std.testing.expectEqualStrings("assets/music/second-nature/second-nature.mp3", fx.pendingAudio().?.path);
// Pause parks the transport (optimistic flag, engine verb, timer
// gone); resume brings all three back.
try h.audio(.loaded, 0, 80_000, true);
try h.menu("sb.toggle");
try std.testing.expect(!Bridge.model().playing);
try std.testing.expect(!fx.audioSnapshot().playing);
try std.testing.expect(!h.playclockArmed());
try h.menu("sb.toggle");
try std.testing.expect(Bridge.model().playing);
try std.testing.expect(fx.audioSnapshot().playing);
// prev within the first 3 seconds goes to the PREVIOUS track;
// past 3 seconds it restarts the current one through the seek verb.
try h.audio(.position, 1_000, 80_000, true);
try h.menu("sb.prev");
try std.testing.expectEqual(@as(?i64, 16), Bridge.model().now);
try h.audio(.loaded, 0, 131_000, true);
try h.audio(.position, 5_000, 131_000, true);
try h.menu("sb.prev");
try std.testing.expectEqual(@as(?i64, 16), Bridge.model().now);
try std.testing.expectEqual(@as(i64, 0), Bridge.model().elapsedMs);
try std.testing.expectEqual(@as(u64, 0), fx.audioSnapshot().position_ms);
}
test "one audio channel: stale reports from a replaced playback drop by the model guard" {
const h = try Harness.create();
defer h.destroy();
const fx = &h.app_state.effects;
// Track 1 plays and reports; then track 3 replaces it. Until the new
// playback's `loaded` acknowledgment, position and completed reports
// can only belong to the replaced track — the guard drops both.
try h.menu("sb.play.1");
try h.audio(.loaded, 0, 165_000, true);
try h.audio(.position, 9_000, 165_000, true);
try h.menu("sb.play.3");
try std.testing.expect(Bridge.model().loadPending);
try std.testing.expectEqual(@as(i64, 0), Bridge.model().elapsedMs);
// A straggler position from the old playback: the rendered clock
// must not scrub the new track.
try h.audio(.position, 9_500, 165_000, true);
try std.testing.expectEqual(@as(i64, 0), Bridge.model().elapsedMs);
// A straggler completion: the player must not double-advance.
try h.audio(.completed, 164_832, 165_000, false);
try std.testing.expectEqual(@as(?i64, 3), Bridge.model().now);
// The new playback's own acknowledgment reopens the stream.
try h.audio(.loaded, 0, 71_000, true);
try std.testing.expect(!Bridge.model().loadPending);
try h.audio(.position, 2_000, 71_000, true);
try std.testing.expectEqual(@as(i64, 2_000), Bridge.model().elapsedMs);
// Spectrum frames are consciously ignored (parity with the Zig
// original — the soundboard's identity is the clean catalog).
var bands: [32]u8 = undefined;
for (&bands, 0..) |*b, i| b.* = @intCast(i * 3);
try fx.feedAudioSpectrum(bands, 3_000, 71_000);
try h.wake();
try std.testing.expectEqual(@as(i64, 2_000), Bridge.model().elapsedMs);
// A failure is never silence: playback clears and the honest stream
// notice reads where the track title did.
try h.audio(.failed, 0, 0, false);
try std.testing.expectEqual(@as(?i64, null), Bridge.model().now);
try std.testing.expect(Bridge.model().streamFailed);
try std.testing.expect(h.hasText("stream unavailable"));
try std.testing.expect(h.hasText("check the connection and try again"));
// Browsing keeps working, and the next play clears the notice.
try std.testing.expect(h.hasText("8 of 8"));
try h.menu("sb.play.1");
try std.testing.expect(!Bridge.model().streamFailed);
}
// -------------------------------------------------------------- clipboard
test "Copy Title lands on the clipboard" {
const h = try Harness.createFake();
defer h.destroy();
const fx = &h.app_state.effects;
try h.menu("sb.play.5");
try h.audio(.loaded, 0, 195_000, true);
// Copy Title (the markup row's context-menu item) writes the track
// title to the clipboard channel — fire-and-forget, one request.
// (The app carries no volume control — parity with the Zig original;
// the audioSetVolume verb stays proven by the host e2e suite's
// fixture core.)
try std.testing.expectEqual(@as(usize, 0), fx.pendingClipboardCount());
try h.menu("sb.copy.5");
try std.testing.expectEqual(@as(usize, 1), fx.pendingClipboardCount());
try std.testing.expectEqualStrings("Untitled", fx.pendingClipboardAt(0).?.text);
try std.testing.expectEqual(@as(i64, 1), Bridge.model().copiesRequested);
}
// --------------------------------------------------------------- search
test "search drives the full text engine from raw platform input and filters both libraries" {
const h = try Harness.create();
defer h.destroy();
// Focus the markup search field and type through the platform's
// text-input channel — the core's byte-splice engine applies each
// event at the caret the runtime reports.
try h.click(h.findId(.search_field, "").?);
try h.textInput("night");
try std.testing.expectEqualStrings("night", Bridge.model().search.bytes);
try std.testing.expect(h.hasText("1 of 8"));
try std.testing.expect(h.hasText("Night Bloom"));
try std.testing.expect(!h.hasText("Exit Signs"));
// The songs library filters on title, artist, AND album title: every
// Night Bloom track matches through its album.
try h.menu("sb.songs");
try std.testing.expect(h.hasText("8 of 68"));
try std.testing.expect(h.hasText("Japanese Maple"));
// Word-delete clears the term through the caret-aware path; the
// catalog returns whole.
try h.keyDown("backspace");
try std.testing.expectEqualStrings("nigh", Bridge.model().search.bytes);
try std.testing.expect(h.hasText("8 of 68"));
try h.textInput("XYZ");
try std.testing.expect(h.hasText("0 of 68"));
try std.testing.expect(h.hasText("No matches for \"nighXYZ\""));
try std.testing.expect(h.hasText("Try an album, artist, or song title."));
// Deleting the term restores the catalog whole.
for (0..7) |_| try h.keyDown("backspace");
try std.testing.expectEqualStrings("", Bridge.model().search.bytes);
try std.testing.expect(h.hasText("68 of 68"));
}
test "automation set_text replaces the search term: the select-all sentinel translates into the core's i64 mirror" {
const h = try Harness.create();
defer h.destroy();
// Seed a term through real typing so the replace verb's select-all
// has bytes to select.
try h.click(h.findId(.search_field, "").?);
try h.textInput("night");
try std.testing.expectEqualStrings("night", Bridge.model().search.bytes);
// `native automate widget-action <search> set_text` routes focus,
// cmd/ctrl+A, then the replacement through the REAL input path.
// Select-all synthesizes `set_selection` carrying the
// `focus = maxInt(usize)` "to the end" sentinel; this core declares
// its TextSelection mirror as i64, so the declared-union translation
// must SATURATE the sentinel — @intCast panicked "integer does not
// fit in destination type" here, killing every set_text against a
// TS app on the live-GUI smoke (real typing was fine; this is the
// headless twin of that crash).
var buffer: [96]u8 = undefined;
const command = try std.fmt.bufPrint(&buffer, "widget-action {s} {d} set-text bloom", .{ canvas_label, h.findId(.search_field, "night").? });
try h.harness.runtime.dispatchAutomationCommand(h.app, command);
// The core's byte-splice engine heard the whole sequence: the snapped
// select-all plus the inserted bytes REPLACE the term (append would
// read "nightbloom" — that would mean the selection mirror was lost).
try std.testing.expectEqualStrings("bloom", Bridge.model().search.bytes);
try std.testing.expect(h.hasText("Night Bloom"));
}
// ------------------------------------------------------- record / replay
const JournalBuffer = struct {
bytes: [512 * 1024]u8 = undefined,
len: usize = 0,
fn sink(self: *JournalBuffer) runtime_ns.SessionRecorderSink {
return .{ .context = self, .write_fn = write };
}
fn write(context: *anyopaque, bytes: []const u8) anyerror!void {
const self: *JournalBuffer = @ptrCast(@alignCast(context));
if (self.len + bytes.len > self.bytes.len) return error.NoSpaceLeft;
@memcpy(self.bytes[self.len .. self.len + bytes.len], bytes);
self.len += bytes.len;
}
fn journalBytes(self: *const JournalBuffer) []const u8 {
return self.bytes[0..self.len];
}
};
/// A value snapshot of the committed soundboard model (committed slices
/// live in the core's heap — copy what outlives a session).
const SoundboardSnapshot = struct {
now: ?i64,
playing: bool,
elapsed_ms: i64,
now_duration_ms: i64,
queue_len: usize,
copies_requested: i64,
fn take() SoundboardSnapshot {
const m = Bridge.model();
return .{
.now = m.now,
.playing = m.playing,
.elapsed_ms = m.elapsedMs,
.now_duration_ms = m.nowDurationMs,
.queue_len = m.queue.len,
.copies_requested = m.copiesRequested,
};
}
};
/// One reference session: journaled user input (menu commands) plus the
/// scripted audio event feed — play, load, ticks, a queue, the
/// completion advance, a clipboard copy.
fn recordSession(buffer: *JournalBuffer) !SoundboardSnapshot {
const recorder = try std.heap.page_allocator.create(runtime_ns.SessionRecorder);
defer std.heap.page_allocator.destroy(recorder);
recorder.* = runtime_ns.SessionRecorder.init(buffer.sink());
recorder.begin(.{ .platform_name = "test", .app_name = "soundboard-ts-e2e", .window_width = 1080, .window_height = 720 });
const h = try Harness.createRecorded(recorder);
defer h.destroy();
try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested);
try h.menu("sb.play.2");
try h.audio(.loaded, 0, 47_000, true);
try h.audio(.position, 1_500, 47_000, true);
try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested);
try h.menu("sb.queue.16");
try h.audio(.completed, 46_944, 47_000, false);
try h.audio(.loaded, 0, 131_000, true);
try h.menu("sb.copy.16");
try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested);
recorder.finish();
try std.testing.expect(!recorder.failed);
return SoundboardSnapshot.take();
}
test "a recorded soundboard session replays byte-identically with zero host calls" {
const buffer = try std.heap.page_allocator.create(JournalBuffer);
defer std.heap.page_allocator.destroy(buffer);
buffer.len = 0;
const recorded = try recordSession(buffer);
try std.testing.expectEqual(@as(?i64, 16), recorded.now);
try std.testing.expect(recorded.playing);
try std.testing.expectEqual(@as(i64, 130_872), recorded.now_duration_ms);
try std.testing.expectEqual(@as(i64, 1), recorded.copies_requested);
// Determinism pin: the same driven session records byte-identical
// journal bytes.
const second = try std.heap.page_allocator.create(JournalBuffer);
defer std.heap.page_allocator.destroy(second);
second.len = 0;
const recorded_again = try recordSession(second);
try std.testing.expectEqualDeep(recorded, recorded_again);
try std.testing.expectEqualSlices(u8, buffer.journalBytes(), second.journalBytes());
// Replay into a fresh app: the journaled audio events feed the
// re-issued (parked) playback in recorded order — no platform player,
// no network, no host calls.
const harness = try native_sdk.TestHarness().create(std.testing.allocator, .{
.size = geometry.SizeF.init(1080, 720),
});
defer harness.destroy(std.testing.allocator);
harness.null_platform.gpu_surfaces = true;
const app_state = try std.testing.allocator.create(App);
defer std.testing.allocator.destroy(app_state);
app_state.* = Adapter.init(std.heap.page_allocator, .{}, appOptions());
defer app_state.deinit();
const report = try runtime_ns.replaySession(&harness.runtime, app_state.app(), buffer.journalBytes(), .{
.verify = true,
.require_same_platform = false,
});
try std.testing.expect(report.ok());
try std.testing.expect(report.events_replayed > 0);
// The journaled effect results are exactly the four audio events.
try std.testing.expectEqual(@as(u64, 4), report.effects_fed);
try std.testing.expectEqualDeep(recorded, SoundboardSnapshot.take());
}
// ------------------------------------------------------------------ perf
test "dispatch at the rendered-clock cadence stays far under the frame budget" {
const h = try Harness.create();
defer h.destroy();
try h.menu("sb.play.1");
try h.audio(.loaded, 0, 165_000, true);
// Whole-pipeline dispatches (update + commit + command walk +
// subscription reconcile + view rebuild) at the clock cadence — the
// hot path a per-frame Zig app exercises through on_frame. The
// position feed is the heavier arm (it re-renders the transport).
const iterations: usize = 400;
const start_ns = runtime_ns.monotonicNanoseconds();
for (0..iterations) |index| {
try h.audio(.position, @intCast(1_000 + index * 250), 165_000, true);
}
const elapsed_ns = runtime_ns.monotonicNanoseconds() - start_ns;
const per_dispatch_ns = elapsed_ns / iterations;
try std.testing.expectEqual(@as(i64, 1_000 + 399 * 250), Bridge.model().elapsedMs);
// The core alone (update + commit + command walk + subscription
// reconcile), without the runtime pipeline around it - the cost the
// transpiled tier adds per tick.
const fx = &h.app_state.effects;
const core_start_ns = runtime_ns.monotonicNanoseconds();
for (0..iterations) |index| {
Bridge.dispatch(fx, .{ .clock_tick = @floatFromInt(60_000 + index * 250) });
}
const core_elapsed_ns = runtime_ns.monotonicNanoseconds() - core_start_ns;
const per_core_dispatch_ns = core_elapsed_ns / iterations;
h.app_state.model = Bridge.model().*;
// Every whole-pipeline dispatch (update + commit + effects + the full
// 68-row markup rebuild) must fit ONE 60Hz frame budget even in a
// Debug build on loaded CI hardware; the core alone must be
// microseconds. Measured on an M-class laptop (Debug): ~3.3ms whole
// pipeline, ~450ns core-only.
try std.testing.expect(per_dispatch_ns < 16_000_000);
try std.testing.expect(per_core_dispatch_ns < 1_000_000);
}
// -------------------------------------------- round 7B: parity closure
// The four launch-gate blockers the port named, closed and pinned here:
// scrub-to-seek (the value-payload change event), controlled scroll (the
// declared ScrollState mirror), and the generated-wiring channels the
// app adopted (frame width -> grid columns, the key fallback, cover
// assets, the launch env override).
test "scrub-to-seek: the slider's value event seeks the engine and jumps the clock" {
const h = try Harness.create();
defer h.destroy();
try h.menu("sb.play.1");
try h.audio(.loaded, 0, 165_000, true);
try std.testing.expectEqual(@as(i64, 164_832), Bridge.model().nowDurationMs);
// Step the seek slider through the automation increment verb — the
// keyboard set_value intent resolving through markup's on-change
// VALUE constructor (the applied f32 0.05 widening into the core's
// one-number float arm).
const slider = h.findLabel("Seek").?;
var buffer: [96]u8 = undefined;
const command = try std.fmt.bufPrint(&buffer, "widget-action {s} {d} increment", .{ canvas_label, slider });
try h.harness.runtime.dispatchAutomationCommand(h.app, command);
// fraction 0.05 -> permille 50 of the manifest total's thousandth
// (164ms): the whole-integer target of the core's two-domain split.
try std.testing.expectEqual(@as(i64, 8_200), Bridge.model().elapsedMs);
// And the engine's single player actually sought there (the audio_ctl
// seek verb, key-gated to the open stream).
try std.testing.expectEqual(@as(u64, 8_200), h.app_state.effects.audio.position_ms);
// The rendered clock renders the jump.
try std.testing.expect(h.hasText("0:08"));
}
test "controlled scroll: the offset echoes into the model and album open resets it" {
const h = try Harness.create();
defer h.destroy();
// Wheel the album grid: the runtime applies the offset, and the
// declared ScrollState mirror arm echoes the applied value into the
// committed model (never fighting the reconcile rule).
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_input = .{
.window_id = 1,
.label = canvas_label,
.timestamp_ns = 1_000_000_000,
.kind = .scroll,
.x = 540,
.y = 300,
.delta_y = 240,
} });
try std.testing.expect(Bridge.model().libraryScrollTop > 0);
// Opening an album is a page change: the model resets the offset (the
// port's named consequence — the detail page opens at its top).
try h.click(h.findLabel("Exit Signs").?);
try std.testing.expectEqual(@as(f64, 0), Bridge.model().libraryScrollTop);
}
test "the frame channel adapts the album grid to the canvas width" {
const h = try Harness.create();
defer h.destroy();
// The installing frame is excluded by the UiApp contract; the first
// presented frame delivers 1080 and the Zig original's width rule
// answers four columns.
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{
.label = canvas_label,
.size = geometry.SizeF.init(1080, 720),
.scale_factor = 1,
.frame_index = 2,
.timestamp_ns = 2_000_000,
} });
try std.testing.expectEqual(@as(i64, 1080), Bridge.model().canvasWidth);
try std.testing.expectEqual(@as(usize, 4), findGrid(h.app_state.tree.?.root).?.layout.columns);
// A live resize re-derives: 1400pt fits five 232pt tiles.
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{
.label = canvas_label,
.size = geometry.SizeF.init(1400, 720),
.scale_factor = 1,
.frame_index = 3,
.timestamp_ns = 3_000_000,
} });
try std.testing.expectEqual(@as(i64, 1400), Bridge.model().canvasWidth);
try std.testing.expectEqual(@as(usize, 5), findGrid(h.app_state.tree.?.root).?.layout.columns);
}
test "the key fallback drives the transport: space toggles, arrows change tracks" {
const h = try Harness.create();
defer h.destroy();
// Nothing focused: space falls through the widget precedence rule to
// the core's keyMsg (delivered lowercased) and starts playback.
try h.keyDown("Space");
try h.audio(.loaded, 0, 0, true);
try std.testing.expect(Bridge.model().playing);
try std.testing.expectEqual(@as(?i64, 1), Bridge.model().now);
// Right arrow advances within the album; space pauses.
try h.keyDown("ArrowRight");
try std.testing.expectEqual(@as(?i64, 2), Bridge.model().now);
try h.keyDown("Space");
try std.testing.expect(!Bridge.model().playing);
}
test "cover assets register at install and bind the grid avatars by album id" {
// PNG twins through the adapter's boot-image channel (the committed
// art is JPEG for real hosts' codecs; the null platform's strict
// decoder takes the engine's own PNG output — same register path the
// generated wiring drives from app.zon).
const rgba = [_]u8{ 200, 40, 40, 255 } ** 4;
var encoded: [256]u8 = undefined;
var png_writer = std.Io.Writer.fixed(&encoded);
try canvas.png.writeRgba8(&png_writer, 2, 2, &rgba);
const boot_images = [_]Adapter.BootImage{.{ .id = 1, .bytes = png_writer.buffered() }};
const h = try Harness.createConfigured(null, .real, .{ .boot_images = &boot_images });
defer h.destroy();
// Registered on the installing frame, before the first view build.
try std.testing.expectEqual(@as(usize, 1), h.harness.runtime.canvas_image_count);
// The grid avatar binds the album id as its ImageId (unregistered ids
// keep the initials fallback, so the other seven tiles degrade
// honestly).
var covers: usize = 0;
countAvatarImageIds(h.app_state.tree.?.root, 1, &covers);
try std.testing.expectEqual(@as(usize, 1), covers);
}
test "the launch env override rides the envMsgs channel: local-only failures report the assets notice" {
// An EMPTY NATIVE_SDK_MUSIC_URL_BASE turns streaming off — the Zig
// original's launch split, delivered here as one journaled Msg.
const env_values = [_]Adapter.EnvValue{.{ .msg = "url_base_set", .value = "" }};
const h = try Harness.createConfigured(null, .real, .{ .env_values = &env_values });
defer h.destroy();
try h.menu("sb.play.1");
// The issued play carries NO stream URL (local-only).
try std.testing.expectEqual(@as(usize, 0), h.app_state.effects.audio.url_len);
try h.audio(.failed, 0, 0, false);
try std.testing.expect(h.hasText("music assets not prepared"));
try std.testing.expect(!h.hasText("stream unavailable"));
}
test "a non-empty env base replaces the stream host wholesale" {
const env_values = [_]Adapter.EnvValue{.{ .msg = "url_base_set", .value = "https://mirror.test/" }};
const h = try Harness.createConfigured(null, .real, .{ .env_values = &env_values });
defer h.destroy();
try h.menu("sb.play.1");
// Trailing slashes trim; the track path's "assets" prefix drops.
try std.testing.expectEqualStrings(
"https://mirror.test/music/exit-signs/mile-marker-west.mp3",
h.app_state.effects.audio.url_buffer[0..h.app_state.effects.audio.url_len],
);
// A failure with the stream configured stays the stream notice.
try h.audio(.failed, 0, 0, false);
try std.testing.expect(h.hasText("stream unavailable"));
}
test "album tiles are quiet on hover (the original's quiet-tile treatment)" {
const h = try Harness.create();
defer h.destroy();
// Every grid tile carries the quiet-surface knob: no hover wash on
// image-forward cover art, while press/selection fills, the focus
// ring, and hit testing keep their own channels. Track rows keep
// their washes (the wash IS the affordance on acting controls).
const grid = findGrid(h.app_state.tree.?.root).?;
try std.testing.expect(grid.children.len > 0);
for (grid.children) |tile| {
try std.testing.expectEqual(canvas.WidgetKind.list_item, tile.kind);
try std.testing.expect(tile.style.quiet_hover);
}
try h.menu("sb.songs");
var row_count: usize = 0;
countLoudListItems(h.app_state.tree.?.root, &row_count);
try std.testing.expect(row_count >= 60);
}
fn countLoudListItems(widget: canvas.Widget, count: *usize) void {
if (widget.kind == .list_item and !widget.style.quiet_hover) count.* += 1;
for (widget.children) |child| {
countLoudListItems(child, count);
}
}
fn findGrid(widget: canvas.Widget) ?canvas.Widget {
if (widget.kind == .grid) return widget;
for (widget.children) |child| {
if (findGrid(child)) |found| return found;
}
return null;
}
fn countAvatarImageIds(widget: canvas.Widget, id: canvas.ImageId, count: *usize) void {
if (widget.kind == .avatar and widget.image_id == id) count.* += 1;
for (widget.children) |child| {
countAvatarImageIds(child, id, count);
}
}
// --------------------------------------------------- transport bar layout
fn transportBarFrame(h: *Harness) ?geometry.RectF {
const layout = h.harness.runtime.canvasWidgetLayout(1, canvas_label) catch return null;
for (layout.nodes) |node| {
if (std.mem.eql(u8, node.widget.semantics.label, "Now playing bar")) return node.frame;
}
return null;
}
fn expectBarFlushAtBottom(h: *Harness, canvas_height: f32) !void {
const frame = transportBarFrame(h) orelse return error.TransportBarMissing;
try std.testing.expectEqual(@as(f32, 76), frame.height);
try std.testing.expectApproxEqAbs(canvas_height - 76, frame.y, 0.01);
}
test "the transport bar stays flush at the bottom across a rebuild storm" {
const h = try Harness.create();
defer h.destroy();
// Playing state: the rebuild storm below is the live app's worst
// case — clock ticks and position events rebuild every 250ms.
try h.menu("sb.playalbum.2");
try h.audio(.loaded, 0, 0, true);
try expectBarFlushAtBottom(h, 720);
// The regression this pins (the runtime's presented-size adoption):
// during a live resize the presented FRAMES carry the drawable's new
// size before the window-manager resize event lands. Every
// dispatch-driven rebuild inside that gap used to lay out at the
// stale bounds, painting the transport bar below the shrunk
// window's bottom edge (cut off) until the resize event arrived.
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{
.label = canvas_label,
.size = geometry.SizeF.init(900, 600),
.scale_factor = 1,
.frame_index = 2,
.timestamp_ns = 2_000_000,
} });
try expectBarFlushAtBottom(h, 600);
// A storm of dispatch-driven rebuilds in the gap (no resize event
// yet): clock ticks, a position correction, page changes, search
// keystrokes, a queue change — the bar must hold the new bottom
// through every one.
_ = try h.firePlayclock(3_000_000_000);
try expectBarFlushAtBottom(h, 600);
try h.audio(.position, 1500, 0, true);
try expectBarFlushAtBottom(h, 600);
try h.menu("sb.open.2");
try expectBarFlushAtBottom(h, 600);
try h.menu("sb.albums");
try expectBarFlushAtBottom(h, 600);
try h.menu("sb.songs");
try expectBarFlushAtBottom(h, 600);
try h.menu("sb.queue.9");
try expectBarFlushAtBottom(h, 600);
try h.click(h.findLabel("Search library").?);
try h.textInput("g");
try expectBarFlushAtBottom(h, 600);
// The late resize event is a no-reflow confirmation.
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_resized = .{
.window_id = 1,
.label = canvas_label,
.frame = geometry.RectF.init(0, 0, 900, 600),
.scale_factor = 1,
} });
try expectBarFlushAtBottom(h, 600);
// Growing back mid-playback holds the same law from the other side.
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{
.label = canvas_label,
.size = geometry.SizeF.init(1080, 720),
.scale_factor = 1,
.frame_index = 3,
.timestamp_ns = 3_000_000,
} });
try expectBarFlushAtBottom(h, 720);
_ = try h.firePlayclock(4_000_000_000);
try expectBarFlushAtBottom(h, 720);
}
// ------------------------------------------------------------ theme accent
test "the manifest accent layers the original's pink over the pack (high contrast skips it)" {
const h = try Harness.create();
defer h.destroy();
// The resolved rebuild tokens carry the accent identity bundle: the
// pink accent under white knockout ink, the focus ring moved with
// the identity, and the seek slider's filled range restated — the
// Zig original's tokens_fn, reached through app.zon's theme_accent.
const pink = canvas.Color.rgb8(0xdf, 0x26, 0x70);
const tokens = h.app_state.effectiveTokens();
try std.testing.expectEqual(pink, tokens.colors.accent);
try std.testing.expectEqual(canvas.Color.rgb8(255, 255, 255), tokens.colors.accent_text);
try std.testing.expectEqual(pink, tokens.colors.focus_ring);
try std.testing.expectEqual(pink, tokens.controls.slider.active_background);
// High contrast takes the pack's own loud register with no brand
// layer (accessibility beats brand — the original's rule).
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .appearance_changed = .{
.color_scheme = .light,
.high_contrast = true,
} });
const loud = h.app_state.effectiveTokens();
try std.testing.expect(!std.meta.eql(pink, loud.colors.accent));
}
// --------------------------------------------------- parity captures
// Env-gated parity capture (skipped by default, never in CI): renders
// the port OFFSCREEN through the deterministic reference renderer via
// the automation screenshot artifact — the side-by-side proof against
// the Zig original's identical states. PNGs land in
// /tmp/parity-shots/ts-{grid,detail,songs}-{0,1}-artifacts/. To use
// (the magick loop prepares RGBA twins of the committed covers once,
// the homepage-shots recipe):
//
// mkdir -p /tmp/soundboard-art
// for f in examples/soundboard/src/art/*.jpg; do
// magick "$f" -depth 8 rgba:/tmp/soundboard-art/"$(basename "${f%.jpg}")".rgba
// done
// PARITY_SHOTS=1 zig build test-ts-core-e2e
test "render parity screenshots (env-gated)" {
if (!envGateSet("PARITY_SHOTS")) return error.SkipZigTest;
const io = std.testing.io;
const sizes = [_]geometry.SizeF{ geometry.SizeF.init(1080, 720), geometry.SizeF.init(1400, 800) };
const cover_stems = [_][]const u8{
"exit-signs", "blue-season", "second-nature", "no-good-way-out",
"glass-flowers", "night-bloom", "motion-picture", "channel-surfing",
};
for (sizes, 0..) |size, size_index| {
const h = try Harness.create();
defer h.destroy();
// The standard macOS tall hidden-inset chrome geometry: the
// resize re-query dispatches it through the core's chromeMsg.
h.harness.null_platform.window_chrome = .{
.insets = .{ .top = 52, .left = 78 },
.buttons = geometry.RectF.init(20, 19, 52, 14),
};
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_resized = .{
.window_id = 1,
.label = canvas_label,
.frame = geometry.RectF.init(0, 0, size.width, size.height),
.scale_factor = 1,
} });
// Real covers on the real decode->register path: RGBA twins of
// the committed JPEG art through the engine's own PNG writer
// (the null platform's strict decoder refuses JPEG honestly).
var art_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
defer art_arena_state.deinit();
const art_arena = art_arena_state.allocator();
for (cover_stems, 1..) |stem, album_id| {
var path_buffer: [128]u8 = undefined;
const path = try std.fmt.bufPrint(&path_buffer, "/tmp/soundboard-art/{s}.rgba", .{stem});
const rgba = std.Io.Dir.cwd().readFileAlloc(io, path, art_arena, .limited(8 * 1024 * 1024)) catch continue;
const side = std.math.sqrt(rgba.len / 4);
const encoded = try art_arena.alloc(u8, try canvas.png.encodedRgba8ByteLen(side, side));
var png_writer = std.Io.Writer.fixed(encoded);
try canvas.png.writeRgba8(&png_writer, side, side, rgba);
_ = try h.app_state.effects.registerImageBytes(album_id, png_writer.buffered());
}
// The shared reference state (identical on the Zig original's
// side): Summer Rental (track 9, album 2) playing at 24636ms of
// its 98544ms manifest total — exactly 0.25 on both progress
// derivations — under the light scheme.
try h.menu("sb.play.9");
try h.audio(.loaded, 0, 0, true);
try h.audio(.position, 24_636, 0, true);
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .appearance_changed = .{ .color_scheme = .light } });
try presentParityFrame(h, size, 2);
try parityShot(h, io, "ts-grid", size_index);
try h.menu("sb.open.2");
try presentParityFrame(h, size, 3);
try parityShot(h, io, "ts-detail", size_index);
try h.menu("sb.albums");
try h.menu("sb.songs");
try presentParityFrame(h, size, 4);
try parityShot(h, io, "ts-songs", size_index);
}
}
/// Env-gated dump switch (the original suite's helper): `std.c.getenv`
/// needs libc, which this test build links on macOS; when absent the
/// gate reads unset and the gated test skips.
fn envGateSet(name: [*:0]const u8) bool {
if (comptime !@import("builtin").link_libc) return false;
return std.c.getenv(name) != null;
}
fn presentParityFrame(h: *Harness, size: geometry.SizeF, frame_index: u64) !void {
try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{
.label = canvas_label,
.size = size,
.scale_factor = 1,
.frame_index = frame_index,
.timestamp_ns = frame_index * 1_000_000,
.nonblank = true,
} });
}
fn parityShot(h: *Harness, io: std.Io, comptime name: []const u8, size_index: usize) !void {
var dir_buffer: [96]u8 = undefined;
const dir = try std.fmt.bufPrint(&dir_buffer, "/tmp/parity-shots/{s}-{d}-artifacts", .{ name, size_index });
h.harness.runtime.options.automation = native_sdk.automation.Server.init(io, dir, "SoundboardTS");
try h.harness.runtime.dispatchAutomationCommand(h.app, "screenshot soundboard-canvas 2");
}