story-kit: merge 109_story_add_test_coverage_for_lozengeflycontext_selectionscreen_and_chatheader_components
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user