Press `?` in the reader to open a Sheet documenting the existing bindings (arrows/J/K paging, Home/End, chapter nav, Esc); Esc or the close button dismisses it. While open, the reader swallows navigation keys so paging can't happen behind the overlay. e2e covers open-on-? and close-on-Esc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
3.3 KiB
TypeScript
62 lines
3.3 KiB
TypeScript
import { test, expect, type Page } from './fixtures';
|
|
|
|
// The reader exposes a keyboard-shortcut help overlay: `?` opens it, Esc
|
|
// (or the close button) dismisses it.
|
|
|
|
const MANGA_ID = 'm1';
|
|
const CHAPTER_ID = 'c1';
|
|
|
|
async function mockReader(page: Page) {
|
|
await page.route('**/api/v1/**', async (route) => {
|
|
const { pathname } = new URL(route.request().url());
|
|
const json = (status: number, body: unknown) =>
|
|
route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) });
|
|
|
|
if (pathname.includes('/files/')) {
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: 'image/svg+xml',
|
|
body: '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="1200"/>'
|
|
});
|
|
}
|
|
if (pathname.endsWith('/auth/config')) return json(200, { self_register_enabled: true, private_mode: false });
|
|
if (pathname.endsWith('/auth/me')) return json(401, { error: { code: 'unauthenticated', message: 'no' } });
|
|
if (pathname.endsWith('/auth/me/preferences')) return json(401, { error: { code: 'unauthenticated', message: 'no' } });
|
|
if (pathname.endsWith(`/mangas/${MANGA_ID}/chapters/${CHAPTER_ID}/pages`)) {
|
|
return json(200, {
|
|
pages: [
|
|
{ id: 'p1', chapter_id: CHAPTER_ID, page_number: 1, storage_key: 'k/1', content_type: 'image/svg+xml' },
|
|
{ id: 'p2', chapter_id: CHAPTER_ID, page_number: 2, storage_key: 'k/2', content_type: 'image/svg+xml' }
|
|
]
|
|
});
|
|
}
|
|
if (pathname.endsWith(`/mangas/${MANGA_ID}/chapters/${CHAPTER_ID}`)) {
|
|
return json(200, { id: CHAPTER_ID, manga_id: MANGA_ID, number: 1, title: null, page_count: 2, created_at: '2026-01-01T00:00:00Z', size_bytes: 0 });
|
|
}
|
|
if (pathname.includes(`/mangas/${MANGA_ID}/chapters`)) {
|
|
return json(200, { items: [{ id: CHAPTER_ID, manga_id: MANGA_ID, number: 1, title: null, page_count: 2, created_at: '2026-01-01T00:00:00Z', size_bytes: 0 }], page: { limit: 200, offset: 0, total: 1 } });
|
|
}
|
|
if (pathname.endsWith(`/mangas/${MANGA_ID}`)) {
|
|
return json(200, { id: MANGA_ID, title: 'Berserk', status: 'ongoing', alt_titles: [], description: null, cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', authors: [], genres: [], tags: [], content_warnings: [], chapter_storage_bytes: 0 });
|
|
}
|
|
if (pathname.includes(`/me/read-progress/${MANGA_ID}`)) return json(401, { error: { code: 'unauthenticated', message: 'no' } });
|
|
return json(503, { error: { code: 'e2e_unmocked', message: pathname } });
|
|
});
|
|
}
|
|
|
|
test('? opens the keyboard-shortcut overlay and Esc closes it', async ({ page }) => {
|
|
await mockReader(page);
|
|
await page.goto(`/manga/${MANGA_ID}/chapter/${CHAPTER_ID}`);
|
|
await expect(page.getByTestId('reader-page')).toBeVisible();
|
|
|
|
await expect(page.getByTestId('reader-shortcuts')).toHaveCount(0);
|
|
|
|
await page.keyboard.press('Shift+Slash'); // "?"
|
|
await expect(page.getByTestId('reader-shortcuts')).toBeVisible();
|
|
// Lists at least one real binding.
|
|
await expect(page.getByTestId('reader-shortcuts')).toContainText('Next page');
|
|
|
|
await page.keyboard.press('Escape');
|
|
await expect(page.getByTestId('reader-shortcuts')).toBeHidden();
|
|
});
|