// @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('我的');
});
});