/** * @file Rendering tests for the Workspace lane tile: the amber "detected" * badge (it may only appear when the inferred stage is genuinely AHEAD of the * declared one, including when the declaration used a node alias — which is * most of what the ship-feature skill declares), and the reorder handles. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import LaneStripCard from "../LaneStripCard"; import type { Lane, LaneNode } from "../../../lib/types"; /** Four nodes of the ship-feature shape: `tests` is an ALIAS of `gates`. */ function nodes(currentId: string): LaneNode[] { return (["intake", "implementing", "gates", "review"] as const).map((id) => ({ id, label: id, icon: "", gate: false, state: id === currentId ? "current" : "pending", })); } function lane(overrides: Partial): Lane { return { id: 1, title: "lane", cwd: "/tmp/lane", branch: null, kind: "adopted", tool: "claude", source_repo: null, pipeline: "ship-feature", session_id: null, run_id: null, stage: "gates", stage_since: null, status: "running", gate_decision: null, ci_status: null, needs_action: null, links: {}, stages: {}, notes: null, pipeline_name: "Ship feature", pipeline_nodes: nodes("gates"), progress: 50, stage_seconds: null, last_event_seconds: null, liveness: "active", detected_stage: null, detected_signal: null, slot: null, ports: {}, active_feature_id: null, ...overrides, } as Lane; } describe("LaneStripCard detected badge", () => { it("badges a detection that is ahead of the declaration", () => { render( {}} onReorder={() => {}} /> ); expect(screen.getByTestId("lane-tile-auto-1")).toHaveTextContent("review"); }); it("stays silent when the declaration was made under an alias and the detection is behind it", () => { // `tests` is an alias of the `gates` node — not a node id. Locating the // declaration by id would score it -1 and badge this stale `implementing` // detection as though the lane had moved backwards. render( {}} onReorder={() => {}} /> ); expect(screen.queryByTestId("lane-tile-auto-1")).toBeNull(); }); it("still badges a detection ahead of an alias declaration", () => { render( {}} onReorder={() => {}} /> ); expect(screen.getByTestId("lane-tile-auto-1")).toHaveTextContent("review"); }); }); describe("LaneStripCard reorder", () => { /** jsdom ships no DataTransfer, so the drag events carry a stub store. */ function dataTransfer(initial = "") { let payload = initial; return { setData: (_type: string, v: string) => { payload = v; }, getData: () => payload, effectAllowed: "", dropEffect: "", }; } it("reports the dragged tile's index and the drop target's", () => { const onReorder = vi.fn(); const dt = dataTransfer(); render( <> {}} onReorder={onReorder} /> {}} onReorder={onReorder} /> ); fireEvent.dragStart(screen.getByTestId("lane-tile-1"), { dataTransfer: dt }); fireEvent.drop(screen.getByTestId("lane-tile-2"), { dataTransfer: dt }); expect(onReorder).toHaveBeenCalledWith(0, 2); }); it("ignores a drop carrying no tile index", () => { const onReorder = vi.fn(); render( {}} onReorder={onReorder} /> ); fireEvent.drop(screen.getByTestId("lane-tile-1"), { dataTransfer: dataTransfer("nonsense") }); expect(onReorder).not.toHaveBeenCalled(); }); it("moves the focused tile with Alt+Arrow, the keyboard path for reordering", () => { const onReorder = vi.fn(); render( {}} onReorder={onReorder} /> ); const tile = screen.getByTestId("lane-tile-1"); fireEvent.keyDown(tile, { key: "ArrowLeft", altKey: true }); expect(onReorder).toHaveBeenCalledWith(2, 1); fireEvent.keyDown(tile, { key: "ArrowRight", altKey: true }); expect(onReorder).toHaveBeenCalledWith(2, 3); // A bare arrow is the strip's own scroll/focus movement, not a reorder. fireEvent.keyDown(tile, { key: "ArrowLeft" }); expect(onReorder).toHaveBeenCalledTimes(2); }); it("shows the id without the 'Lane' prefix", () => { render( {}} onReorder={() => {}} /> ); const tile = screen.getByTestId("lane-tile-7"); expect(tile).toHaveTextContent("#7"); expect(tile.textContent).not.toMatch(/Lane/i); }); });