import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { DRAFT_HERO_TRANSITION_ANIMATION_ID, runMobileComposerTransition, waitForDraftHeroTransition, } from "./draftHeroTransition"; afterEach(() => { vi.unstubAllGlobals(); }); describe("waitForDraftHeroTransition", () => { it("waits for active draft hero animations and ignores unrelated animations", async () => { let finishTransition: (() => void) | undefined; const transitionFinished = new Promise((resolve) => { finishTransition = resolve; }); vi.stubGlobal("document", { getAnimations: () => [ { id: "unrelated-animation", finished: new Promise(() => undefined) }, { id: DRAFT_HERO_TRANSITION_ANIMATION_ID, finished: transitionFinished }, ], }); let handoffComplete = false; const handoff = waitForDraftHeroTransition().then(() => { handoffComplete = true; }); await Promise.resolve(); expect(handoffComplete).toBe(false); finishTransition?.(); await handoff; expect(handoffComplete).toBe(true); }); it("allows the handoff when an active transition is cancelled", async () => { vi.stubGlobal("document", { getAnimations: () => [ { id: DRAFT_HERO_TRANSITION_ANIMATION_ID, finished: Promise.reject(new Error("cancelled")), }, ], }); await expect(waitForDraftHeroTransition()).resolves.toBeUndefined(); }); }); describe("runMobileComposerTransition", () => { it("keeps the route handoff waiting while the mobile composer morph is active", async () => { let finishTransition: (() => void) | undefined; const transitionFinished = new Promise((resolve) => { finishTransition = resolve; }); const dataset: Record = {}; vi.stubGlobal("document", { documentElement: { dataset }, getAnimations: () => [], startViewTransition: (update: () => void | Promise) => { void update(); return { finished: transitionFinished }; }, }); vi.stubGlobal("window", { matchMedia: (query: string) => ({ matches: query === "(max-width: 639px)" }), }); const transition = runMobileComposerTransition(() => undefined); await Promise.resolve(); let handoffComplete = false; const handoff = waitForDraftHeroTransition().then(() => { handoffComplete = true; }); await Promise.resolve(); expect(handoffComplete).toBe(false); finishTransition?.(); await Promise.all([transition, handoff]); expect(handoffComplete).toBe(true); }); it("uses a scoped view transition on mobile", async () => { const dataset: Record = {}; const startViewTransition = vi.fn((update: () => void | Promise) => ({ finished: Promise.resolve(update()).then(() => undefined), })); vi.stubGlobal("document", { documentElement: { dataset }, startViewTransition, }); vi.stubGlobal("window", { matchMedia: (query: string) => ({ matches: query === "(max-width: 639px)" }), }); const update = vi.fn(); await runMobileComposerTransition(update); expect(startViewTransition).toHaveBeenCalledOnce(); expect(update).toHaveBeenCalledOnce(); expect(dataset).not.toHaveProperty("mobileComposerRouteTransition"); }); it("updates without a view transition when reduced motion is preferred", async () => { const startViewTransition = vi.fn(); vi.stubGlobal("document", { documentElement: { dataset: {} }, startViewTransition, }); vi.stubGlobal("window", { matchMedia: () => ({ matches: true }), }); const update = vi.fn(); await runMobileComposerTransition(update); expect(startViewTransition).not.toHaveBeenCalled(); expect(update).toHaveBeenCalledOnce(); }); });