From 16df865a88f4eeb4fc55d4628fe032e6932db4fa Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:54:39 -0700 Subject: [PATCH] test(coding-agent): cover inline-picker wrapper mouse offset Add regression tests for all five inline-picker wrappers asserting a click on the top border row is inert and a click on the first list row below it confirms the first item, pinning the top-border line offset. --- .../show-images-selector-mouse.test.ts | 52 +++++++++ .../wrapper-selector-mouse-offset.test.ts | 105 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 packages/coding-agent/test/modes/components/show-images-selector-mouse.test.ts create mode 100644 packages/coding-agent/test/modes/components/wrapper-selector-mouse-offset.test.ts diff --git a/packages/coding-agent/test/modes/components/show-images-selector-mouse.test.ts b/packages/coding-agent/test/modes/components/show-images-selector-mouse.test.ts new file mode 100644 index 000000000..d3ba4c1ea --- /dev/null +++ b/packages/coding-agent/test/modes/components/show-images-selector-mouse.test.ts @@ -0,0 +1,52 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { ShowImagesSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/show-images-selector"; +import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; +import type { SgrMouseEvent } from "@oh-my-pi/pi-tui"; + +beforeAll(async () => { + await initTheme(); +}); + +function leftClick(line: number): SgrMouseEvent { + return { button: 0, col: 0, row: line, release: false, wheel: null, motion: false, leftClick: true }; +} + +/** + * The wrapper mounts a single-line top DynamicBorder before its SelectList, so + * routed component-local lines are offset by one. Regression guard for the + * off-by-one that would let a top-border click select the first row. + */ +describe("ShowImagesSelectorComponent.routeMouse offset", () => { + it("ignores a click on the top border row (line 0)", () => { + let selected: boolean | undefined; + const component = new ShowImagesSelectorComponent( + true, + value => { + selected = value; + }, + () => {}, + ); + component.render(80); + + component.routeMouse(leftClick(0), 0, 0); + + expect(selected).toBeUndefined(); + }); + + it("selects the first item when the row below the border is clicked (line 1)", () => { + let selected: boolean | undefined; + const component = new ShowImagesSelectorComponent( + true, + value => { + selected = value; + }, + () => {}, + ); + component.render(80); + + component.routeMouse(leftClick(1), 1, 0); + + // First SelectList row is "Yes" → true. + expect(selected).toBe(true); + }); +}); diff --git a/packages/coding-agent/test/modes/components/wrapper-selector-mouse-offset.test.ts b/packages/coding-agent/test/modes/components/wrapper-selector-mouse-offset.test.ts new file mode 100644 index 000000000..76805d3e9 --- /dev/null +++ b/packages/coding-agent/test/modes/components/wrapper-selector-mouse-offset.test.ts @@ -0,0 +1,105 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { Effort } from "@oh-my-pi/pi-ai"; +import { PluginSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/plugin-selector"; +import { QueueModeSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/queue-mode-selector"; +import { ThemeSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/theme-selector"; +import { ThinkingSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/thinking-selector"; +import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; +import type { SgrMouseEvent } from "@oh-my-pi/pi-tui"; + +beforeAll(async () => { + await initTheme(); +}); + +function leftClick(line: number): SgrMouseEvent { + return { button: 0, col: 0, row: line, release: false, wheel: null, motion: false, leftClick: true }; +} + +/** + * Every wrapper mounts a single-line top DynamicBorder before its SelectList, + * so routed component-local lines are offset by one. These guard the + * off-by-one that would let a top-border click select the first row. Each case + * asserts line 0 (border) is inert and line 1 (first list row) confirms. + */ +describe("inline-picker wrapper routeMouse offset", () => { + it("ThemeSelectorComponent ignores the border row and selects the first theme below it", () => { + let selected: string | undefined; + const component = new ThemeSelectorComponent( + "alpha", + ["alpha", "beta"], + value => { + selected = value; + }, + () => {}, + () => {}, + ); + component.render(80); + + component.routeMouse(leftClick(0), 0, 0); + expect(selected).toBeUndefined(); + + component.routeMouse(leftClick(1), 1, 0); + expect(selected).toBe("alpha"); + }); + + it("ThinkingSelectorComponent ignores the border row and selects the first level below it", () => { + let selected: Effort | undefined; + const levels = [Effort.Low, Effort.High]; + const component = new ThinkingSelectorComponent( + Effort.Low, + levels, + value => { + selected = value; + }, + () => {}, + ); + component.render(80); + + component.routeMouse(leftClick(0), 0, 0); + expect(selected).toBeUndefined(); + + component.routeMouse(leftClick(1), 1, 0); + expect(selected).toBe(Effort.Low); + }); + + it("QueueModeSelectorComponent ignores the border row and selects the first mode below it", () => { + let selected: "all" | "one-at-a-time" | undefined; + const component = new QueueModeSelectorComponent( + "all", + value => { + selected = value; + }, + () => {}, + ); + component.render(80); + + component.routeMouse(leftClick(0), 0, 0); + expect(selected).toBeUndefined(); + + // First SelectList row is "one-at-a-time" regardless of the preselected mode. + component.routeMouse(leftClick(1), 1, 0); + expect(selected).toBe("one-at-a-time"); + }); + + it("PluginSelectorComponent ignores the border row and selects the first plugin below it", () => { + let selectedName: string | undefined; + const component = new PluginSelectorComponent( + 1, + [{ plugin: { name: "alpha", description: "first" }, marketplace: "shop" }], + new Set(), + { + onSelect: name => { + selectedName = name; + }, + onCancel: () => {}, + }, + ); + component.render(80); + + component.routeMouse(leftClick(0), 0, 0); + expect(selectedName).toBeUndefined(); + + component.routeMouse(leftClick(1), 1, 0); + expect(selectedName).toBe("alpha"); + }); +});