story-kit: merge 109_story_add_test_coverage_for_lozengeflycontext_selectionscreen_and_chatheader_components

This commit is contained in:
Dave
2026-02-23 22:28:13 +00:00
parent 965d07930e
commit a3b0dc0161
3 changed files with 574 additions and 0 deletions

View File

@@ -710,3 +710,249 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
expect(probe.dataset.hidden).toBe("");
});
});
// ─── Agent swap (name change) triggers both fly-out and fly-in ────────────
describe("LozengeFlyProvider agent swap (name change)", () => {
beforeEach(() => {
vi.useFakeTimers();
Element.prototype.getBoundingClientRect = vi.fn().mockReturnValue({
left: 100,
top: 50,
right: 180,
bottom: 70,
width: 80,
height: 20,
x: 100,
y: 50,
toJSON: () => ({}),
});
vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
cb(0);
return 0;
});
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it("detects agent name change as both fly-out (old) and fly-in (new)", async () => {
const withCoder1 = makePipeline({
current: [
{
story_id: "109_swap_test",
name: "Swap Test",
error: null,
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
},
],
});
const withCoder2 = makePipeline({
current: [
{
story_id: "109_swap_test",
name: "Swap Test",
error: null,
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
},
],
});
const { rerender } = render(
<LozengeFlyProvider pipeline={withCoder1}>
<RosterFixture agentName="coder-1" />
<RosterFixture agentName="coder-2" />
<HiddenAgentsProbe />
<StagePanel title="Current" items={withCoder1.current} />
</LozengeFlyProvider>,
);
// Advance past initial fly-in
await act(async () => {
vi.advanceTimersByTime(600);
});
// Swap agent: coder-1 → coder-2
await act(async () => {
rerender(
<LozengeFlyProvider pipeline={withCoder2}>
<RosterFixture agentName="coder-1" />
<RosterFixture agentName="coder-2" />
<HiddenAgentsProbe />
<StagePanel title="Current" items={withCoder2.current} />
</LozengeFlyProvider>,
);
});
// A fly-out clone for coder-1 should appear (old agent leaves)
const flyOut = document.body.querySelector(
'[data-testid^="flying-lozenge-fly-out"]',
);
expect(flyOut).not.toBeNull();
// A fly-in clone for coder-2 should appear (new agent arrives)
const flyIn = document.body.querySelector(
'[data-testid^="flying-lozenge-fly-in"]',
);
expect(flyIn).not.toBeNull();
});
});
// ─── Fly-out without a roster element (null rosterRect fallback) ──────────
describe("LozengeFlyProvider fly-out without roster element", () => {
beforeEach(() => {
vi.useFakeTimers();
Element.prototype.getBoundingClientRect = vi.fn().mockReturnValue({
left: 200,
top: 100,
right: 280,
bottom: 120,
width: 80,
height: 20,
x: 200,
y: 100,
toJSON: () => ({}),
});
vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
cb(0);
return 0;
});
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it("fly-out still works when no roster element is registered (uses fallback coords)", async () => {
const withAgent = makePipeline({
current: [
{
story_id: "109_no_roster_flyout",
name: "No Roster Flyout",
error: null,
agent: {
agent_name: "orphan-agent",
model: null,
status: "completed",
},
},
],
});
const noAgent = makePipeline({
current: [
{
story_id: "109_no_roster_flyout",
name: "No Roster Flyout",
error: null,
agent: null,
},
],
});
const { rerender } = render(
<LozengeFlyProvider pipeline={withAgent}>
{/* No RosterFixture for orphan-agent */}
<StagePanel title="Current" items={withAgent.current} />
</LozengeFlyProvider>,
);
await act(async () => {
vi.advanceTimersByTime(600);
});
await act(async () => {
rerender(
<LozengeFlyProvider pipeline={noAgent}>
<StagePanel title="Current" items={noAgent.current} />
</LozengeFlyProvider>,
);
});
// Fly-out clone should still appear even without roster element
const clone = document.body.querySelector(
'[data-testid^="flying-lozenge-fly-out"]',
);
expect(clone).not.toBeNull();
});
});
// ─── Flying clone renders in initial (non-flying) state ───────────────────
describe("FlyingLozengeClone initial non-flying render", () => {
beforeEach(() => {
vi.useFakeTimers();
Element.prototype.getBoundingClientRect = vi.fn().mockReturnValue({
left: 100,
top: 50,
right: 180,
bottom: 70,
width: 80,
height: 20,
x: 100,
y: 50,
toJSON: () => ({}),
});
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it("clone has transition: none before rAF fires", async () => {
// Collect rAF callbacks instead of firing them immediately
const rafCallbacks: FrameRequestCallback[] = [];
vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
rafCallbacks.push(cb);
return rafCallbacks.length;
});
const noPipeline = makePipeline();
const withAgent = makePipeline({
current: [
{
story_id: "109_nontransition_test",
name: "Non-transition Test",
error: null,
agent: { agent_name: "coder-1", model: null, status: "running" },
},
],
});
const { rerender } = render(
<LozengeFlyProvider pipeline={noPipeline}>
<RosterFixture agentName="coder-1" />
<StagePanel title="Current" items={[]} />
</LozengeFlyProvider>,
);
// Trigger fly-in but don't flush rAF callbacks
await act(async () => {
rerender(
<LozengeFlyProvider pipeline={withAgent}>
<RosterFixture agentName="coder-1" />
<StagePanel title="Current" items={withAgent.current} />
</LozengeFlyProvider>,
);
});
// Clone should exist in its initial (non-flying) state
const clone = document.body.querySelector(
'[data-testid^="flying-lozenge-fly-in"]',
) as HTMLElement | null;
expect(clone).not.toBeNull();
expect(clone?.style.transition).toBe("none");
// Now flush rAF callbacks to trigger the flying state
await act(async () => {
for (const cb of rafCallbacks) cb(0);
rafCallbacks.length = 0;
// Flush inner rAF callbacks too
for (const cb of rafCallbacks) cb(0);
});
});
});