// @vitest-environment jsdom import { act } from 'react'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { render } from '../src/shared/ui/render-root.ts'; import { AppShell } from '../src/app/App.tsx'; import { connectionLabel, navMode } from '../src/app/AppNav.tsx'; import { useSyncStore } from '../src/shared/stores/sync.store.ts'; import { RuntimeContext } from '../src/app/providers/runtime-context.tsx'; import { useAuthStore } from '../src/shared/stores/auth.store.ts'; const root = document.createElement('div'); document.body.append(root); const listeners = { addEventListener: () => undefined, removeEventListener: () => undefined }; const runtime = { api: { baseUrl: 'https://reader.example.com' }, mediaApi: { preferenceScope: () => 'shell-test' }, player: { ...listeners, setVisible: () => undefined }, playback: { snapshot: () => ({}) }, } as any; const user = { id: 'u', username: 'lin', displayName: '林间', role: 'member', createdAt: 0 }; const paint = (path: string) => { act(() => render(null, root)); // a fresh router per call act(() => render( }>页面} /> , root, )); }; beforeEach(() => useAuthStore.setState({ session: { user } as any, verifiedUser: user as any })); afterEach(() => { act(() => render(null, root)); useAuthStore.setState({ session: null, verifiedUser: null }); delete document.body.dataset.appNav; delete document.documentElement.dataset.theme; delete document.documentElement.dataset.mode; }); describe('shell navigation', () => { it('decides where the bar shows: top-level pages, pushed pages, and the full-screen ones', () => { expect(['/', '/shelf', '/me', '/media/video', '/media/music/albums', '/media/audiobook/books'].map(navMode)).toEqual(Array(6).fill('top')); expect(['/library', '/sources', '/media/video/items/x', '/media/video/settings', '/media/video/favorites', '/media/queue'].map(navMode)).toEqual(Array(6).fill('push')); expect(['/book/abc', '/media/video/player', '/media/music/player/lyrics'].map(navMode)).toEqual(Array(3).fill('hidden')); }); it('puts the navigation after the page, marks 影音 and the channel current, and themes the page', () => { paint('/media/music/albums'); const page = root.querySelector('[data-testid="page"]')!; const nav = root.querySelector('nav.app-nav')!; expect(nav.getAttribute('aria-label')).toBe('主导航'); expect(page.closest('.app-shell-body')).not.toBeNull(); expect(page.compareDocumentPosition(nav) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect([...nav.querySelectorAll('.app-nav-item')].map(a => a.textContent)).toEqual(['首页', '书架', '影音', '我的']); expect(nav.querySelector('.app-nav-item[aria-current="page"]')?.textContent).toBe('影音'); expect(nav.querySelector('.app-nav-subitem[aria-current="page"]')?.textContent).toBe('音乐'); expect(document.body.dataset.appNav).toBe('top'); expect(document.documentElement.dataset.theme).toBe('warm'); }); it('keeps the sidebar on pushed pages, and removes the bar for the book and the player', () => { paint('/media/video/items/film-1'); expect(root.querySelector('nav.app-nav')?.getAttribute('data-mode')).toBe('push'); expect(document.body.dataset.appNav).toBe('push'); paint('/media/video/player'); expect(root.querySelector('nav.app-nav')).toBeNull(); expect(document.body.dataset.appNav).toBeUndefined(); }); it('gives the book the same theme as every other page', () => { paint('/book/abc'); expect(root.querySelector('nav.app-nav')).toBeNull(); expect(document.documentElement.dataset.theme).toBe('warm'); }); it('says whether the server is reachable under the account, and searches books and media together', () => { expect(['idle', 'syncing', 'offline', 'error', 'signed-out'].map(state => connectionLabel(state as never).label)).toEqual(['已连接', '正在同步', '离线', '同步出错', '已连接']); act(() => useSyncStore.getState().update({ state: 'offline', lastSyncAt: null, pending: false, message: '' })); paint('/media/video'); expect(root.querySelector('.app-nav-user small')?.textContent).toBe('离线'); act(() => useSyncStore.getState().update({ state: 'idle', lastSyncAt: null, pending: false, message: '' })); expect(root.querySelector('.app-nav-user small')?.textContent).toBe('已连接'); expect(root.querySelector('.app-nav-search input')?.placeholder).toBe('搜索书籍和影音'); }); it('marks the reading destinations under 书架 and the account page under 我的', () => { paint('/sources'); expect(root.querySelector('.app-nav-item[aria-current="page"]')?.textContent).toBe('书架'); paint('/me'); expect(root.querySelector('.app-nav-item[aria-current="page"]')?.textContent).toBe('我的'); }); });