Files
reader/web/test/folder-picker.test.tsx
hectorqinandClaude 99361a46dd feat(web): readable errors, sync status page, folder picker for moves
- describeError: show the server's own Chinese wording, replace transport
  and proxy diagnostics with what kind of failure it was; used by search,
  shelf, library, sources and the media page frame
- account page gains a sync panel (state, reason, last success, retry);
  the sidebar and 我的 link to it when sync is offline or failing
- per-route document titles; the reader uses the book title
- reader says when the restored position came from another device
- file manager: pick a move destination by walking folders instead of
  typing a library-relative path

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-10 03:46:34 +00:00

64 lines
3.5 KiB
TypeScript

// @vitest-environment jsdom
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { BrowseEntry, BrowseListing } from '../src/api/types.ts';
import { FolderPicker, loadFolderChoices, type FolderChoices } from '../src/features/library/components/FolderPicker.tsx';
const entry = (path: string, type: 'dir' | 'file'): BrowseEntry => ({ name: path.split('/').pop()!, path, type } as BrowseEntry);
const listing = (path: string, entries: BrowseEntry[], total: number, dirs: number): BrowseListing =>
({ path, crumbs: [{ name: '书库', path: '' }], parent: null, entries, total, dirs, files: total - dirs, size: 0, writable: true, name: '书库' });
describe('loadFolderChoices', () => {
it('collects the folders of every listing page and stops once it has them all', async () => {
const pages: Record<number, BrowseListing> = {
1: listing('', [entry('a.epub', 'file'), entry('b', 'dir')], 5, 2),
2: listing('', [entry('c.epub', 'file'), entry('d', 'dir')], 5, 2),
3: listing('', [entry('e.epub', 'file')], 5, 2),
};
const browse = vi.fn(async (_path: string, page: number) => pages[page]!);
const choices = await loadFolderChoices(browse, '', new AbortController().signal);
expect(choices.dirs.map(dir => dir.path)).toEqual(['b', 'd']);
expect(browse).toHaveBeenCalledTimes(2);
});
});
describe('FolderPicker', () => {
let host: HTMLElement, root: Root;
const tree: Record<string, FolderChoices> = {
'': { crumbs: [{ name: '书库', path: '' }], dirs: [{ name: '小说', path: '小说' }, { name: '漫画', path: '漫画' }], writable: true },
'小说': { crumbs: [{ name: '书库', path: '' }, { name: '小说', path: '小说' }], dirs: [], writable: true },
};
const load = vi.fn(async (path: string) => tree[path]!);
const flush = () => act(async () => { await Promise.resolve(); await Promise.resolve(); });
const confirm = () => [...host.querySelectorAll('button')].find(button => button.textContent === '移动到这里')!;
const row = (name: string) => [...host.querySelectorAll<HTMLButtonElement>('.folder-picker-row')].find(button => button.textContent?.includes(name))!;
beforeEach(() => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
host = document.createElement('div'); document.body.append(host); root = createRoot(host); load.mockClear();
});
afterEach(() => { act(() => root.unmount()); host.remove(); });
it('does not offer the folder the items are already in, nor a folder being moved', async () => {
await act(async () => root.render(<FolderPicker moving={['漫画']} from="" load={load} onPick={() => undefined} onClose={() => undefined} />));
await flush();
expect(confirm().disabled).toBe(true);
expect(host.textContent).toContain('已经在这个文件夹里');
expect(row('漫画').disabled).toBe(true);
expect(row('小说').disabled).toBe(false);
});
it('answers with the folder walked to', async () => {
const onPick = vi.fn();
await act(async () => root.render(<FolderPicker moving={['a.epub']} from="" load={load} onPick={onPick} onClose={() => undefined} />));
await flush();
await act(async () => row('小说').click());
await flush();
expect(host.textContent).toContain('没有子文件夹');
expect(confirm().disabled).toBe(false);
await act(async () => confirm().click());
expect(onPick).toHaveBeenCalledWith('小说');
});
});