mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
test(e2e): keyboard Tab reaches programmes beyond the rendered EPG range
Greptile flagged that windowing the ribbon could strand keyboard users at the last rendered block. Focusing a block scrolls it into view, which re-windows before the next key press; the new test walks ten unrendered programmes past the range with Tab and with Shift+Tab, and fails if focus ever leaves the ribbon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca72d75e86
commit
371f8151a5
2 files changed
+102
-1
No files matched your search
@@ -17,6 +17,7 @@ import {
|
||||
} from './electron-test-fixtures';
|
||||
import {
|
||||
fetchXtreamEpgFixture,
|
||||
fetchXtreamLiveFixture,
|
||||
fetchStalkerCategoryFixture,
|
||||
} from './portal-mock-fixtures';
|
||||
|
||||
@@ -463,3 +464,100 @@ test('@epg @stalker @theme @electron applies live themes to the shared Stalker g
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
/** Title of the programme block holding keyboard focus, if any. */
|
||||
async function focusedTimelineBlockTitle(page: Page): Promise<string | null> {
|
||||
return page.evaluate(() => {
|
||||
const block = document.activeElement?.closest(
|
||||
'app-epg-timeline .epg-timeline__block'
|
||||
);
|
||||
return (
|
||||
block
|
||||
?.querySelector('.epg-timeline__block-title')
|
||||
?.textContent?.trim() ?? null
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Two frames: the focus scroll's measure, then the re-rendered blocks. */
|
||||
async function nextFrames(page: Page): Promise<void> {
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
test('@epg @xtream @electron reaches programmes beyond the rendered range with Tab', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
// The ribbon renders only the programmes near the visible range; keyboard
|
||||
// focus scrolls a block into view, which renders its neighbours before
|
||||
// the next key press. A generated schedule (240 half-hour slots) is far
|
||||
// wider than that range.
|
||||
test.setTimeout(180000);
|
||||
await resetMockServers(request, ['xtream']);
|
||||
const credentials = { username: 'minimal', password: 'minimal' };
|
||||
const fixture = await fetchXtreamLiveFixture(request, credentials);
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await app.mainWindow.route('https://test-streams.mux.dev/**', () => {
|
||||
// Keep the external demo request pending; guide data is local.
|
||||
});
|
||||
await addXtreamPortal(app.mainWindow, {
|
||||
name: 'Xtream Timeline Keyboard',
|
||||
...credentials,
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
await openWorkspaceSection(app.mainWindow, 'Live TV');
|
||||
await clickCategoryByNameExact(app.mainWindow, fixture.categoryName);
|
||||
const channelRow = channelItemByTitle(
|
||||
app.mainWindow,
|
||||
fixture.items[0]?.name ?? ''
|
||||
).first();
|
||||
await expect(channelRow).toBeVisible({ timeout: 20000 });
|
||||
await channelRow.click();
|
||||
|
||||
const timeline = app.mainWindow.locator('app-epg-timeline');
|
||||
const nowBlock = timeline.locator('.epg-timeline__block.is-now');
|
||||
await expect(nowBlock).toBeVisible({ timeout: 20000 });
|
||||
const titles = timeline.locator('.epg-timeline__block-title');
|
||||
const initial = new Set(
|
||||
(await titles.allInnerTexts()).map((title) => title.trim())
|
||||
);
|
||||
expect(initial.size).toBeLessThan(120);
|
||||
|
||||
// Walk well past the rendered range in both directions: ten
|
||||
// programmes that were not in the DOM when the walk started.
|
||||
for (const key of ['Tab', 'Shift+Tab']) {
|
||||
// The previous walk moved the window away from now.
|
||||
await timeline.locator('.epg-timeline__jump').click();
|
||||
await expect(nowBlock).toBeVisible();
|
||||
await nowBlock.focus();
|
||||
const reached = new Set<string>();
|
||||
for (let press = 0; press < 400 && reached.size < 10; press++) {
|
||||
await app.mainWindow.keyboard.press(key);
|
||||
await nextFrames(app.mainWindow);
|
||||
const inRibbon = await app.mainWindow.evaluate(
|
||||
() =>
|
||||
!!document.activeElement?.closest(
|
||||
'app-epg-timeline .epg-timeline__ribbon'
|
||||
)
|
||||
);
|
||||
expect(inRibbon, `${key} press ${press} left the ribbon`).toBe(
|
||||
true
|
||||
);
|
||||
const title = await focusedTimelineBlockTitle(app.mainWindow);
|
||||
if (title && !initial.has(title)) {
|
||||
reached.add(title);
|
||||
}
|
||||
}
|
||||
expect(reached.size, `${key} programmes beyond the range`).toBe(10);
|
||||
}
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
@@ -378,7 +378,10 @@ remain local when the meaning is explicit.
|
||||
them all competed with the stream's first frame (J3 in
|
||||
[performance journeys](performance-journeys.md#j3-playback-start-playback-to-the-first-frame)).
|
||||
Tests and features must not assume an off-screen programme is in the DOM:
|
||||
scroll the ribbon to it first.
|
||||
scroll the ribbon to it first. Keyboard access must keep working: focusing
|
||||
a block scrolls it into view, which renders its neighbours before the next
|
||||
Tab (`epg-timeline-interaction.e2e.ts` walks past the rendered range both
|
||||
ways).
|
||||
|
||||
### Collapsible Live Sidebar
|
||||
|
||||
|
||||
Reference in new issue
Block a user