mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-10 21:11:08 +08:00
我的 mixed sheets (书架显示, 系统设置, OPDS nested in the shelf sheet) with pages (外观与主题, 影音浏览, 播放), and the personal preferences were four separate rows. - 偏好设置 (#/me/settings) holds 外观与主题, 书架, 影音浏览 and 播放 as sections of one page, with a jump bar and ?section=; every control applies at once (影音浏览 loses its save button and unsaved-changes prompt). The shelf's gear still opens the same fields (ShelfSettingsFields) as a sheet shortcut. - 系统设置 becomes three admin pages of 我的 › 管理: 用户管理, 注册与邀请, 服务配置 (#/me/admin/*, lazily loaded). - 连接外部阅读器 (OPDS) is a page (#/me/opds) under 账号, also reached from the 书架 section and the shelf sheet; back returns to whoever opened it. - Old /me/theme|browse|playback and /media/:channel/settings[/:panel] redirect to the matching section; /me/account keeps its page. - Drops MediaSettingsPage and the CSS only it used; docs follow the new paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
4.3 KiB
TypeScript
89 lines
4.3 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, expect, it, vi } from 'vitest';
|
|
import { act, type ReactNode } from 'react';
|
|
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
|
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
|
|
import { render } from '../src/shared/ui/render-root.ts';
|
|
import { RuntimeContext } from '../src/app/providers/runtime-context.tsx';
|
|
import { useAuthStore } from '../src/shared/stores/auth.store.ts';
|
|
import { ShelfPage } from '../src/features/shelf/pages/ShelfPage.tsx';
|
|
import { LoginPage } from '../src/features/auth/pages/LoginPage.tsx';
|
|
|
|
vi.mock('../src/features/shelf/queries/shelf.queries.ts', () => ({
|
|
useShelf: () => ({ data: { items: [], total: 0, pageSize: 60 }, isPending: false, error: null }),
|
|
useShelfGroups: () => ({ data: undefined, isPending: false, error: null }),
|
|
}));
|
|
|
|
const root = document.createElement('div');
|
|
document.body.append(root);
|
|
if (typeof ResizeObserver === 'undefined') {
|
|
globalThis.ResizeObserver = class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
} as unknown as typeof ResizeObserver;
|
|
}
|
|
|
|
function runtime() {
|
|
const api = {
|
|
baseUrl: 'https://reader.test',
|
|
currentSession: () => ({ user: { id: 'u1', username: 'admin', displayName: '管理员', role: 'admin' } }),
|
|
refreshQueries: vi.fn(),
|
|
continueReading: vi.fn(async () => []),
|
|
browseBatchShelf: vi.fn(async () => ({ books: [] })),
|
|
setBaseUrl: vi.fn(),
|
|
instance: vi.fn(async () => ({ name: 'reader', apiVersion: 1, userCount: 1, registrationOpen: false, invitationRequired: false })),
|
|
opdsCredentials: vi.fn(async () => ({ credentials: [], catalogUrl: '/opds' })),
|
|
createOpdsCredential: vi.fn(),
|
|
revokeOpdsCredential: vi.fn(),
|
|
adminUsers: vi.fn(async () => ({ users: [] })),
|
|
registrationSettings: vi.fn(async () => ({ mode: 'closed', invites: [] })),
|
|
};
|
|
return {
|
|
api,
|
|
updateSettings: vi.fn(async () => undefined),
|
|
settings: { setServerUrl: vi.fn(async () => undefined) },
|
|
} as any;
|
|
}
|
|
|
|
function mount(node: ReactNode, value = runtime(), initialEntry = '/') {
|
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
act(() => render(<RuntimeContext value={value}><QueryClientProvider client={queryClient}><><MemoryRouter initialEntries={[initialEntry]}>{node}</MemoryRouter></></QueryClientProvider></RuntimeContext>, root));
|
|
return value;
|
|
}
|
|
|
|
afterEach(() => {
|
|
act(() => render(null, root));
|
|
useAuthStore.setState({ session: null, verifiedUser: null });
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function OpdsProbe() { return <p data-opds-return={useLocation().state?.returnTo} />; }
|
|
|
|
it('keeps the shelf entry points available and restores clear, settings and OPDS actions', async () => {
|
|
mount(<Routes><Route path="/shelf" element={<ShelfPage />} /><Route path="/me/opds" element={<OpdsProbe />} /></Routes>, runtime(), '/shelf?q=三体');
|
|
expect(root.textContent).toContain('书源');
|
|
expect(root.textContent).not.toContain('刷新');
|
|
expect(root.querySelector('button[aria-label^="书架设置"]')).not.toBeNull();
|
|
expect(root.querySelector('[aria-label="清除搜索"]')).not.toBeNull();
|
|
const search = root.querySelector<HTMLInputElement>('input[type="search"]')!;
|
|
await act(async () => root.querySelector<HTMLButtonElement>('[aria-label="清除搜索"]')!.click());
|
|
expect(search.value).toBe('');
|
|
|
|
const settingsButton = root.querySelector<HTMLButtonElement>('button[aria-label^="书架设置"]');
|
|
await act(async () => settingsButton?.click());
|
|
expect(root.textContent).toContain('连接外部阅读器');
|
|
await act(async () => [...root.querySelectorAll('button')].find(button => button.textContent === '连接外部阅读器')?.click());
|
|
// The OPDS page of 我的, which returns to the shelf.
|
|
await vi.waitFor(() => expect(root.querySelector('[data-opds-return]')?.getAttribute('data-opds-return')).toBe('/shelf'));
|
|
});
|
|
|
|
it('collapses the server address and explains that sessions are remembered on login', () => {
|
|
mount(<LoginPage />);
|
|
const details = root.querySelector<HTMLDetailsElement>('.login-connection')!;
|
|
expect(details.open).toBe(false);
|
|
expect(root.textContent).toContain('登录状态自动保持,无需每天重新登录');
|
|
details.querySelector('summary')!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
expect(details.open).toBe(true);
|
|
});
|