diff --git a/src/lib/ui/motion/viewTransition.ts b/src/lib/ui/motion/viewTransition.ts index bfa8916..e576efa 100644 --- a/src/lib/ui/motion/viewTransition.ts +++ b/src/lib/ui/motion/viewTransition.ts @@ -2,8 +2,7 @@ // browser's View Transitions API so route changes cross-fade cleanly and // shared-element transitions (Fleet card → Server detail hero) morph. // -// Chromium ships the API today; Firefox and Safari fall through to a plain -// navigation — same behavior as before, just without the transition. +// Browsers without the API use ordinary navigation. // Structural type — accepts whatever SvelteKit passes to onNavigate without // coupling to a specific @sveltejs/kit version's type name. @@ -13,15 +12,20 @@ export function enableViewTransitions(navigation: NavigationLike): Promise // Feature-detect without a custom Document extension — the DOM lib type // varies between TS versions, and any narrowing has to survive both. const start = (document as unknown as { - startViewTransition?: (cb: () => void | Promise) => unknown; + startViewTransition?: (cb: () => void | Promise) => { ready: Promise; finished: Promise }; }).startViewTransition; if (typeof start !== 'function') return; return new Promise((resolve) => { - start.call(document, async () => { - resolve(); - await navigation.complete; - }); + try { + const transition = start.call(document, async () => { + resolve(); + // Another navigation can supersede this one during its animation. + await navigation.complete.catch(() => {}); + }); + void transition.ready.catch(() => {}); + void transition.finished.catch(() => {}).finally(resolve); + } catch { resolve(); } }); } diff --git a/tests/unit/ui-view-transition.test.ts b/tests/unit/ui-view-transition.test.ts new file mode 100644 index 0000000..da884b0 --- /dev/null +++ b/tests/unit/ui-view-transition.test.ts @@ -0,0 +1,23 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import { enableViewTransitions } from '../../src/lib/ui/motion/viewTransition.js'; + +afterEach(() => vi.unstubAllGlobals()); + +it('allows navigation to continue when a transition is skipped or superseded', async () => { + let finish: () => void = () => {}; + const finished = new Promise(resolve => { finish = resolve; }); + const complete = Promise.reject(new Error('navigation aborted')); + vi.stubGlobal('document', { startViewTransition: (update: () => Promise) => { + void update().then(finish); + return { ready: Promise.reject(new DOMException('Skipped transition', 'AbortError')), finished }; + } }); + await enableViewTransitions({ complete }); + await finished; +}); + +it('continues without animation when the browser cannot start a transition', async () => { + vi.stubGlobal('document', { startViewTransition: () => { throw new Error('Unavailable'); } }); + await enableViewTransitions({ complete: Promise.resolve() }); + vi.stubGlobal('document', {}); + expect(enableViewTransitions({ complete: Promise.resolve() })).toBeUndefined(); +});