import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; describe('App route transition feedback', () => { it('tracks pathname and query changes without remounting page content for search-only navigation', () => { const source = readFileSync(resolve(process.cwd(), 'src/web/App.tsx'), 'utf8').replace(/\r\n/g, '\n'); expect(source).toContain('const routeTransitionKey = `${location.pathname}${location.search}`;'); expect(source).toContain('const previousRouteTransitionKeyRef = useRef(routeTransitionKey);'); expect(source).toContain('const sidebarRef = useRef(null);'); expect(source).toContain('const mainRef = useRef(null);'); expect(source).toContain("import { shouldFocusMainOnRouteChange } from './pages/helpers/routeFocus.js';"); expect(source).toContain('previousRouteTransitionKeyRef.current === routeTransitionKey'); expect(source).toContain('className="skip-link"'); expect(source).toContain('href="#main-content"'); expect(source).toContain('mainRef.current?.focus({ preventScroll: true });'); expect(source).toContain("{t('跳到主内容')}"); expect(source).toContain('ref={mainRef}'); expect(source).toContain('id="main-content"'); expect(source).toContain('tabIndex={-1}'); expect(source).toContain("aria-label={t('主内容')}"); expect(source).toContain('const pageMotionKey = `${location.pathname}${location.search}`;'); expect(source).toContain('key={location.pathname} motionKey={pageMotionKey}'); expect(source).toContain(''); expect(source).not.toContain('function RouteLoadingFallback'); expect(source).not.toContain('fallback={}'); expect(source).not.toContain('routeTransitioning'); expect(source).not.toContain('is-route-transitioning'); expect(source).toContain('className="sr-only"'); expect(source).toContain('aria-live="polite"'); expect(source).toContain('aria-atomic="true"'); }); it('moves focus to the main region on shell-driven route changes without stealing field focus', () => { const source = readFileSync(resolve(process.cwd(), 'src/web/App.tsx'), 'utf8').replace(/\r\n/g, '\n'); expect(source).toContain('const shouldFocusMain = shouldFocusMainOnRouteChange(document.activeElement, {'); expect(source).toContain('body: document.body,'); expect(source).toContain('topbar: topbarRef.current,'); expect(source).toContain('sidebar: sidebarRef.current,'); expect(source).not.toContain('const shouldFocusMain = !activeElement'); expect(source).toContain('let focusFrameId: number | null = null;'); expect(source).toContain("const schedule = typeof window.requestAnimationFrame === 'function'"); expect(source).toContain('focusFrameId = schedule(() => {'); expect(source).toContain('mainRef.current?.focus({ preventScroll: true });'); expect(source).toContain("typeof window.cancelAnimationFrame === 'function'"); expect(source).toContain('window.cancelAnimationFrame(focusFrameId);'); }); it('does not render a blocking shell loading frame while lazy routes resolve', () => { const css = readFileSync(resolve(process.cwd(), 'src/web/index.css'), 'utf8').replace(/\r\n/g, '\n'); const source = readFileSync(resolve(process.cwd(), 'src/web/App.tsx'), 'utf8').replace(/\r\n/g, '\n'); expect(css).toContain('.skip-link'); expect(css).toContain('.skip-link:focus,\n.skip-link:focus-visible'); expect(css).toContain('.sr-only'); expect(css).toContain('.skip-link,\n .skip-link:focus,\n .skip-link:focus-visible'); expect(source).not.toContain('RouteLoadingFallback'); expect(source).not.toContain('animate-fade-in'); expect(source).not.toContain('aria-busy={routeTransitioning}'); expect(css).not.toContain('.route-progress'); expect(css).not.toContain('@keyframes route-progress-sweep'); expect(css).not.toContain('.main-content.is-route-transitioning'); }); });