diff --git a/packages/simulation/src/frontend/actions.ts b/packages/simulation/src/frontend/actions.ts index 64e9fd1cd8b..78f6602ff1d 100644 --- a/packages/simulation/src/frontend/actions.ts +++ b/packages/simulation/src/frontend/actions.ts @@ -1,7 +1,14 @@ import { tmpdir } from "node:os" import { extname, join, resolve } from "node:path" import type { CliRenderer, Renderable } from "@opentui/core" -import { createMockKeys, createMockMouse, type MockInput, type MockMouse } from "@opentui/core/testing" +import { + createMockKeys, + createMockMouse, + KeyCodes, + type KeyInput, + type MockInput, + type MockMouse, +} from "@opentui/core/testing" import { Config, Effect, FileSystem } from "effect" import type { SimulationProtocol } from "../protocol" import { SimulationRenderer } from "./renderer" @@ -26,6 +33,15 @@ type RenderBuffer = { const decoder = new TextDecoder() +function isKeyCode(key: string): key is keyof typeof KeyCodes { + return Object.hasOwn(KeyCodes, key) +} + +function keyInput(key: string): KeyInput { + const named = key.toUpperCase() + return isKeyCode(named) ? named : key +} + function children(renderable: Renderable) { return renderable.getChildren().filter((child): child is Renderable => "num" in child) } @@ -154,7 +170,7 @@ export const execute = Effect.fn("SimulationActions.execute")(function* (harness yield* Effect.tryPromise(() => harness.mockInput.typeText(action.text)) break case "ui.press": - harness.mockInput.pressKey(action.key, action.modifiers) + harness.mockInput.pressKey(keyInput(action.key), action.modifiers) break case "ui.enter": harness.mockInput.pressEnter() diff --git a/packages/simulation/test/actions.test.ts b/packages/simulation/test/actions.test.ts index de777cd7303..98976d0e74e 100644 --- a/packages/simulation/test/actions.test.ts +++ b/packages/simulation/test/actions.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "bun:test" -import { matches } from "../src/frontend/actions" +import { Effect } from "effect" +import { execute, type Harness, matches } from "../src/frontend/actions" test("matches literal screen text", () => { const harness = { screen: () => "OpenCode [ready].*" } @@ -9,3 +10,32 @@ test("matches literal screen text", () => { expect(matches(harness, "OpenCode.*ready")).toBe(false) expect(matches(harness, "opencode")).toBe(false) }) + +test("normalizes named keys for OpenTUI", async () => { + const pressed: Array = [] + 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], + ]) +})