test(magazine): cover driving seek controls

This commit is contained in:
mARTin-B78 2026-09-26 19:39:00 +02:00
parent 9904459913
commit c81907ccfe
3 changed files with 47 additions and 4 deletions

View File

@ -52,7 +52,7 @@ Two self-hosted Docker instances, both port 7890:
| Studio (phase wizard) | — | `studio.js` | mature | | Studio (phase wizard) | — | `studio.js` | mature |
| TTS engines / routing | `routes/tts.py`, `core/routing.py` | `engines.js` | mature | | TTS engines / routing | `routes/tts.py`, `core/routing.py` | `engines.js` | mature |
| STT | `routes/stt.py` | — | mature | | STT | `routes/stt.py` | — | mature |
| **Magazines** | `core/magazine.py`, `routes/magazine.py` | `magazine.js`, `s-magazine.html` | **P0 + P1** | | **Magazines** | `core/magazine.py`, `routes/magazine.py` | `magazine.js`, `s-magazine.html` | **P0 + P1**; generated-MP3 playlist and driving-mode seek/±15 s transport |
## Data model ## Data model

View File

@ -258,9 +258,12 @@ works regardless.
`#mag-driving` is a full-screen overlay, not a separate playback path: it `#mag-driving` is a full-screen overlay, not a separate playback path: it
reuses whatever is already playing or queued, so entering it never reuses whatever is already playing or queued, so entering it never
interrupts an article in progress. Two oversized controls (skip, play/pause) interrupts an article in progress. Its touch-sized transport exposes the
and no other interactive surface, matching the "no interaction required" previous/next article, back/forward 15 seconds, play/pause, and direct seeking
behaviour the manual originally specified as a P2 goal. on the shared playlist `<audio>` element. Seeking is bounded to the current
track. Playback speed and the optional synchronized text/image companion are
browser-local preferences; none of these controls changes server-side article
data or creates a second playback state.
### Automatic sidebar/advert exclusion ### Automatic sidebar/advert exclusion

View File

@ -217,6 +217,46 @@ test.describe('Magazine reader', () => {
await expect(exportPicker.locator('.vp-item[data-value="smoke_voice"]')).toBeVisible(); await expect(exportPicker.locator('.vp-item[data-value="smoke_voice"]')).toBeVisible();
}); });
test('driving mode seeks and skips 15 seconds within the current article', async ({ page }) => {
await mockBackend(page, { articleStatus: 'approved', hasAudio: true });
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
await navTo(page, 's-magazine');
await page.locator('#mag-library-list .mag-card').first().click();
await page.locator('#mag-driving-open').click();
await expect(page.locator('#mag-driving')).toBeVisible();
// Use a deterministic two-minute track so the transport controls can be
// exercised without depending on the browser's media decoder clock.
await page.locator('#mag-playlist-audio').evaluate((audio) => {
audio.pause();
Object.defineProperties(audio, {
duration: { configurable: true, value: 120 },
currentTime: { configurable: true, writable: true, value: 60 },
});
});
const currentTime = () => page.locator('#mag-playlist-audio').evaluate((audio) => audio.currentTime);
await page.getByRole('button', { name: 'Back 15 seconds', exact: true }).click();
await expect.poll(currentTime).toBe(45);
await page.getByRole('button', { name: 'Forward 15 seconds', exact: true }).click();
await expect.poll(currentTime).toBe(60);
await page.locator('#mag-playlist-audio').evaluate((audio) => { audio.currentTime = 5; });
await page.getByRole('button', { name: 'Back 15 seconds', exact: true }).click();
await expect.poll(currentTime).toBe(0);
await page.locator('#mag-playlist-audio').evaluate((audio) => { audio.currentTime = 115; });
await page.getByRole('button', { name: 'Forward 15 seconds', exact: true }).click();
await expect.poll(currentTime).toBe(120);
const seek = page.getByRole('slider', { name: 'Position in article' });
await seek.evaluate((input) => {
input.value = '250';
input.dispatchEvent(new Event('input', { bubbles: true }));
});
await expect.poll(currentTime).toBe(30);
});
test('library lists a magazine and opens its contents tree', async ({ page }) => { test('library lists a magazine and opens its contents tree', async ({ page }) => {
await mockBackend(page); await mockBackend(page);
await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.goto('/', { waitUntil: 'domcontentloaded' });