From 855a9569b53d0fb658f24e2c9c72c7648dd50279 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 9 Jul 2026 10:50:35 -0400 Subject: [PATCH] feat(simulation): add literal screen text matching (#36085) --- packages/simulation/src/frontend/actions.ts | 4 ++++ packages/simulation/src/frontend/server.ts | 2 ++ packages/simulation/src/protocol/index.ts | 7 +++++++ packages/simulation/test/actions.test.ts | 11 +++++++++++ packages/simulation/test/protocol.test.ts | 21 +++++++++++++++++++++ 5 files changed, 45 insertions(+) create mode 100644 packages/simulation/test/actions.test.ts create mode 100644 packages/simulation/test/protocol.test.ts diff --git a/packages/simulation/src/frontend/actions.ts b/packages/simulation/src/frontend/actions.ts index 5819d94035..202b1707f6 100644 --- a/packages/simulation/src/frontend/actions.ts +++ b/packages/simulation/src/frontend/actions.ts @@ -104,6 +104,10 @@ export function state(harness: Harness) { } } +export function matches(harness: Pick, text: string) { + return harness.screen().includes(text) +} + export async function screenshot(harness: Harness, name?: string) { await harness.renderOnce() const image = SimulationPng.screenshot(harness.renderer) diff --git a/packages/simulation/src/frontend/server.ts b/packages/simulation/src/frontend/server.ts index b878bdd761..a70cfc30cb 100644 --- a/packages/simulation/src/frontend/server.ts +++ b/packages/simulation/src/frontend/server.ts @@ -21,6 +21,8 @@ async function handle( case "ui.state": { return SimulationActions.state(harness) } + case "ui.matches": + return SimulationActions.matches(harness, request.params.text) case "ui.recording.finish": if (!finishRecording) throw new Error("UI recording is not available") return finishRecording() diff --git a/packages/simulation/src/protocol/index.ts b/packages/simulation/src/protocol/index.ts index a2b9fa3c29..1414c72cd6 100644 --- a/packages/simulation/src/protocol/index.ts +++ b/packages/simulation/src/protocol/index.ts @@ -97,12 +97,18 @@ export namespace Frontend { export const RecordingFinish = Schema.String export type RecordingFinish = Schema.Schema.Type + export const Matches = Schema.Boolean + export type Matches = Schema.Schema.Type + export const ScreenshotParams = Schema.Struct({ name: Schema.optional(Schema.String) }) export interface ScreenshotParams extends Schema.Schema.Type {} export const TypeParams = Schema.Struct({ text: Schema.String }) export interface TypeParams extends Schema.Schema.Type {} + export const MatchesParams = Schema.Struct({ text: Schema.String }) + export interface MatchesParams extends Schema.Schema.Type {} + export const PressParams = Schema.Struct({ key: Schema.String, modifiers: Schema.optional(KeyModifiers) }) export interface PressParams extends Schema.Schema.Type {} @@ -121,6 +127,7 @@ export namespace Frontend { Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.arrow"), params: ArrowParams }), Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.focus"), params: FocusParams }), Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.click"), params: ClickParams }), + Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.matches"), params: MatchesParams }), Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.screenshot"), diff --git a/packages/simulation/test/actions.test.ts b/packages/simulation/test/actions.test.ts new file mode 100644 index 0000000000..de777cd730 --- /dev/null +++ b/packages/simulation/test/actions.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from "bun:test" +import { matches } from "../src/frontend/actions" + +test("matches literal screen text", () => { + const harness = { screen: () => "OpenCode [ready].*" } + + expect(matches(harness, "OpenCode")).toBe(true) + expect(matches(harness, "[ready].*")).toBe(true) + expect(matches(harness, "OpenCode.*ready")).toBe(false) + expect(matches(harness, "opencode")).toBe(false) +}) diff --git a/packages/simulation/test/protocol.test.ts b/packages/simulation/test/protocol.test.ts new file mode 100644 index 0000000000..5d0ea4c970 --- /dev/null +++ b/packages/simulation/test/protocol.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from "bun:test" +import { Frontend } from "../src/protocol" + +test("decodes ui.matches text params", () => { + expect( + Frontend.decodeRequest({ + jsonrpc: "2.0", + id: 1, + method: "ui.matches", + params: { text: "OpenCode [ready].*" }, + }), + ).toMatchObject({ method: "ui.matches", params: { text: "OpenCode [ready].*" } }) + expect(() => + Frontend.decodeRequest({ + jsonrpc: "2.0", + id: 1, + method: "ui.matches", + params: { pattern: "OpenCode.*" }, + }), + ).toThrow() +})