| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- import { expect, test } from "bun:test"
- import { BoxRenderable, TextRenderable } from "@opentui/core"
- import { Effect } from "effect"
- import { createHarness, execute, type Harness, matches, snapshot, state } from "../src/frontend/actions"
- import { SimulationRenderer } from "../src/frontend/renderer"
- import { SimulationSemantics } from "../src/frontend/semantics"
- 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)
- })
- test("normalizes named keys for OpenTUI", async () => {
- const pressed: Array<readonly [string, object | undefined]> = []
- const harness = {
- renderer: {
- root: { getChildren: () => [] },
- currentFocusedRenderable: undefined,
- currentFocusedEditor: undefined,
- },
- mockInput: {
- pressKey: (key: string, modifiers?: object) => pressed.push([key, modifiers]),
- },
- renderOnce: async () => {},
- } as unknown as Harness
- await Effect.runPromise(
- execute(harness, {
- type: "ui.press",
- key: "escape",
- modifiers: { ctrl: true },
- }),
- )
- await Effect.runPromise(execute(harness, { type: "ui.press", key: "x" }))
- expect(pressed).toEqual([
- ["ESCAPE", { ctrl: true }],
- ["x", undefined],
- ])
- })
- test("headless input mirrors the configured kitty keyboard protocol", async () => {
- await Effect.runPromise(
- Effect.scoped(
- Effect.gen(function* () {
- const renderer = yield* SimulationRenderer.create({ useKittyKeyboard: {} })
- const harness = createHarness(renderer)
- let key: { readonly name: string; readonly source: string } | undefined
- renderer.keyInput.once("keypress", (event) => {
- key = event
- })
- yield* execute(harness, { type: "ui.press", key: "escape" })
- expect(key).toMatchObject({ name: "escape", source: "kitty" })
- }),
- ),
- )
- })
- test("clicks a target at relative coordinates through descendant text", async () => {
- await Effect.runPromise(
- Effect.scoped(
- Effect.gen(function* () {
- const renderer = yield* SimulationRenderer.create({})
- let clicks = 0
- const button = new BoxRenderable(renderer, {
- id: "permission.action.once",
- width: 12,
- height: 1,
- onMouseUp: () => clicks++,
- })
- button.add(new TextRenderable(renderer, { content: "Allow once" }))
- renderer.root.add(button)
- const harness = createHarness(renderer)
- yield* Effect.promise(() => harness.renderOnce())
- expect(state(harness).elements).toContainEqual(expect.objectContaining({ id: button.id, clickable: true }))
- yield* execute(harness, { type: "ui.click", target: button.num, x: 1, y: 0 })
- expect(clicks).toBe(1)
- renderer.root.remove(button)
- const error = yield* execute(harness, { type: "ui.click", target: button.num, x: 1, y: 0 }).pipe(Effect.flip)
- expect(error.message).toContain("click target is stale or unavailable")
- }),
- ),
- )
- })
- test("rejects a semantic click when the live identity does not match", async () => {
- await Effect.runPromise(
- Effect.scoped(
- Effect.gen(function* () {
- const renderer = yield* SimulationRenderer.create({})
- let clicks = 0
- const button = new BoxRenderable(renderer, {
- id: "session.permission.action.once",
- width: 12,
- height: 1,
- onMouseUp: () => clicks++,
- })
- SimulationSemantics.bind(() => ({
- instance: "permission-2",
- role: "option",
- label: "Allow once",
- }))(button)
- renderer.root.add(button)
- const harness = createHarness(renderer)
- yield* Effect.promise(() => harness.renderOnce())
- const error = yield* execute(harness, {
- type: "ui.click",
- target: button.num,
- x: 1,
- y: 0,
- semantic: {
- id: "session.permission.action.once",
- instance: "permission-1",
- element: button.num,
- },
- }).pipe(Effect.flip)
- expect(error.message).toContain("semantic click target is stale or unavailable")
- expect(clicks).toBe(0)
- yield* execute(harness, {
- type: "ui.click",
- target: button.num,
- x: 1,
- y: 0,
- semantic: {
- id: "session.permission.action.once",
- instance: "permission-2",
- element: button.num,
- },
- })
- expect(clicks).toBe(1)
- }),
- ),
- )
- })
- test("snapshots lazy semantic hierarchy and interaction state", async () => {
- await Effect.runPromise(
- Effect.scoped(
- Effect.gen(function* () {
- const renderer = yield* SimulationRenderer.create({})
- let selected = "once"
- const dialog = new BoxRenderable(renderer, { id: "session.permission" })
- const actions = new BoxRenderable(renderer, { id: "session.permission.actions" })
- const once = new BoxRenderable(renderer, { id: "session.permission.action.once" })
- SimulationSemantics.bind(() => ({
- instance: "permission-1",
- role: "dialog",
- label: "Permission required",
- expanded: false,
- }))(dialog)
- SimulationSemantics.bind(() => ({
- instance: "permission-1",
- role: "listbox",
- label: "Permission choices",
- }))(actions)
- SimulationSemantics.bind(() => ({
- instance: "permission-1",
- role: "option",
- label: "Allow once",
- focused: selected === "once",
- selected: selected === "once",
- disabled: false,
- }))(once)
- renderer.root.add(dialog)
- dialog.add(actions)
- actions.add(once)
- expect(snapshot(createHarness(renderer))).toEqual({
- format: "opencode-ui-snapshot-v1",
- nodes: [
- {
- id: "session.permission",
- instance: "permission-1",
- role: "dialog",
- label: "Permission required",
- element: dialog.num,
- expanded: false,
- },
- {
- id: "session.permission.actions",
- instance: "permission-1",
- parent: "session.permission",
- role: "listbox",
- label: "Permission choices",
- element: actions.num,
- },
- {
- id: "session.permission.action.once",
- instance: "permission-1",
- parent: "session.permission.actions",
- role: "option",
- label: "Allow once",
- element: once.num,
- focused: true,
- selected: true,
- disabled: false,
- },
- ],
- })
- selected = "reject"
- expect(snapshot(createHarness(renderer)).nodes.at(-1)).toMatchObject({
- id: "session.permission.action.once",
- focused: false,
- selected: false,
- })
- dialog.visible = false
- expect(snapshot(createHarness(renderer)).nodes).toEqual([])
- }),
- ),
- )
- })
- test("rejects duplicate semantic identities", async () => {
- await Effect.runPromise(
- Effect.scoped(
- Effect.gen(function* () {
- const renderer = yield* SimulationRenderer.create({})
- const first = new BoxRenderable(renderer, { id: "duplicate" })
- const second = new BoxRenderable(renderer, { id: "duplicate" })
- const definition = () => ({ role: "option" })
- SimulationSemantics.bind(definition)(first)
- SimulationSemantics.bind(definition)(second)
- renderer.root.add(first)
- renderer.root.add(second)
- expect(() => snapshot(createHarness(renderer))).toThrow("duplicate semantic UI id: duplicate")
- }),
- ),
- )
- })
- test("validates lazy semantic definitions before returning them", async () => {
- await Effect.runPromise(
- Effect.scoped(
- Effect.gen(function* () {
- const renderer = yield* SimulationRenderer.create({})
- const invalid = new BoxRenderable(renderer, { id: "invalid" })
- SimulationSemantics.bind(() => ({ role: "" }))(invalid)
- renderer.root.add(invalid)
- expect(() => snapshot(createHarness(renderer))).toThrow()
- }),
- ),
- )
- })
|