mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
test(dashboard): select rail internals through stable test ids
The dashboard contract makes data-test-id hooks the supported Electron E2E selector surface. The rail now exposes -viewport, -track and -card-link hooks next to its existing ones, and the focus E2E selects those (and the rail heading by role) instead of internal class names. The dashboard doc lists the new hooks and records the focus-reveal contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f305e68bec
commit
c544d4645b
3 files changed
+73
-54
No files matched your search
@@ -11,6 +11,13 @@ import {
|
||||
|
||||
const sourcesRailId = 'dashboard-recent-sources-rail';
|
||||
const sourceCount = 4;
|
||||
/** The rail's stable `data-test-id` hooks, as CSS selectors. */
|
||||
const hooks = {
|
||||
viewport: `[data-test-id="${sourcesRailId}-viewport"]`,
|
||||
track: `[data-test-id="${sourcesRailId}-track"]`,
|
||||
card: `[data-test-id="${sourcesRailId}-card"]`,
|
||||
cardLink: `[data-test-id="${sourcesRailId}-card-link"]`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Sizes the sources rail's cards so the last one is half visible: the rail
|
||||
@@ -18,21 +25,24 @@ const sourceCount = 4;
|
||||
* an element that already shows 32px or more. Returns the card width.
|
||||
*/
|
||||
async function makeLastCardHalfVisible(rail: Locator): Promise<number> {
|
||||
const width = await rail.evaluate((section, count) => {
|
||||
const host = section.parentElement as HTMLElement;
|
||||
const viewport = section.querySelector(
|
||||
'.rail__viewport'
|
||||
) as HTMLElement;
|
||||
const track = section.querySelector('.rail__track') as HTMLElement;
|
||||
const gap = parseFloat(getComputedStyle(track).columnGap) || 0;
|
||||
// count cards + (count - 1) gaps = viewport + half a card.
|
||||
const cardWidth = Math.floor(
|
||||
(viewport.clientWidth - gap * (count - 1)) / (count - 0.5)
|
||||
);
|
||||
host.style.setProperty('--cover-rail-width', `${cardWidth}px`);
|
||||
track.scrollTo({ left: 0, behavior: 'auto' });
|
||||
return cardWidth;
|
||||
}, sourceCount);
|
||||
const width = await rail.evaluate(
|
||||
(section, { count, selectors }) => {
|
||||
const host = section.parentElement as HTMLElement;
|
||||
const viewport = section.querySelector(
|
||||
selectors.viewport
|
||||
) as HTMLElement;
|
||||
const track = section.querySelector(selectors.track) as HTMLElement;
|
||||
const gap = parseFloat(getComputedStyle(track).columnGap) || 0;
|
||||
// count cards + (count - 1) gaps = viewport + half a card.
|
||||
const cardWidth = Math.floor(
|
||||
(viewport.clientWidth - gap * (count - 1)) / (count - 0.5)
|
||||
);
|
||||
host.style.setProperty('--cover-rail-width', `${cardWidth}px`);
|
||||
track.scrollTo({ left: 0, behavior: 'auto' });
|
||||
return cardWidth;
|
||||
},
|
||||
{ count: sourceCount, selectors: hooks }
|
||||
);
|
||||
await expect
|
||||
.poll(() => lastCardVisibleWidth(rail))
|
||||
.toBeGreaterThan(Math.min(64, width / 4));
|
||||
@@ -41,41 +51,39 @@ async function makeLastCardHalfVisible(rail: Locator): Promise<number> {
|
||||
|
||||
/** Pixels of the last card inside the rail's visible viewport. */
|
||||
function lastCardVisibleWidth(rail: Locator): Promise<number> {
|
||||
return rail.evaluate((section) => {
|
||||
return rail.evaluate((section, selectors) => {
|
||||
const viewport = section
|
||||
.querySelector('.rail__viewport')
|
||||
.querySelector(selectors.viewport)
|
||||
?.getBoundingClientRect();
|
||||
const cards = section.querySelectorAll('.rail__card');
|
||||
const cards = section.querySelectorAll(selectors.card);
|
||||
const card = cards[cards.length - 1]?.getBoundingClientRect();
|
||||
if (!viewport || !card) return 0;
|
||||
return (
|
||||
Math.min(card.right, viewport.right) -
|
||||
Math.max(card.left, viewport.left)
|
||||
);
|
||||
});
|
||||
}, hooks);
|
||||
}
|
||||
|
||||
async function lastCardFullyVisible(rail: Locator): Promise<boolean> {
|
||||
return rail.evaluate((section) => {
|
||||
return rail.evaluate((section, selectors) => {
|
||||
const viewport = section
|
||||
.querySelector('.rail__viewport')
|
||||
.querySelector(selectors.viewport)
|
||||
?.getBoundingClientRect();
|
||||
const cards = section.querySelectorAll('.rail__card');
|
||||
const cards = section.querySelectorAll(selectors.card);
|
||||
const card = cards[cards.length - 1]?.getBoundingClientRect();
|
||||
if (!viewport || !card) return false;
|
||||
return (
|
||||
card.left >= viewport.left - 1 && card.right <= viewport.right + 1
|
||||
);
|
||||
});
|
||||
}, hooks);
|
||||
}
|
||||
|
||||
function lastCardLinkFocused(page: Page): Promise<boolean> {
|
||||
return page.evaluate((railId) => {
|
||||
const links = document.querySelectorAll(
|
||||
`[data-test-id="${railId}"] .rail__card-link`
|
||||
);
|
||||
return page.evaluate((selector) => {
|
||||
const links = document.querySelectorAll(selector);
|
||||
return document.activeElement === links[links.length - 1];
|
||||
}, sourcesRailId);
|
||||
}, hooks.cardLink);
|
||||
}
|
||||
|
||||
test.describe('Dashboard rail focus', () => {
|
||||
@@ -102,13 +110,12 @@ test.describe('Dashboard rail focus', () => {
|
||||
|
||||
await goToDashboard(app.mainWindow);
|
||||
const rail = app.mainWindow.getByTestId(sourcesRailId);
|
||||
await expect(rail.getByTestId(`${sourcesRailId}-card`)).toHaveCount(
|
||||
sourceCount
|
||||
);
|
||||
await expect(rail.locator(hooks.card)).toHaveCount(sourceCount);
|
||||
const cardLinks = rail.locator(hooks.cardLink);
|
||||
|
||||
// Tab from the first card to the last card's link.
|
||||
await makeLastCardHalfVisible(rail);
|
||||
await rail.locator('.rail__card-link').first().focus();
|
||||
await cardLinks.first().focus();
|
||||
for (
|
||||
let presses = 0;
|
||||
presses < sourceCount * 3 &&
|
||||
@@ -122,35 +129,29 @@ test.describe('Dashboard rail focus', () => {
|
||||
|
||||
// `focus()` from script after a mouse click elsewhere, starting
|
||||
// at the rail's start again.
|
||||
await rail.locator('.rail__label').click();
|
||||
await rail.evaluate((section) => {
|
||||
await rail.getByRole('heading').click();
|
||||
await rail.evaluate((section, selector) => {
|
||||
(document.activeElement as HTMLElement | null)?.blur();
|
||||
section
|
||||
.querySelector('.rail__track')
|
||||
.querySelector(selector)
|
||||
?.scrollTo({ left: 0, behavior: 'auto' });
|
||||
});
|
||||
}, hooks.track);
|
||||
await expect.poll(() => lastCardFullyVisible(rail)).toBe(false);
|
||||
await rail
|
||||
.locator('.rail__card-link')
|
||||
.last()
|
||||
.evaluate((link) => {
|
||||
(link as HTMLElement).focus();
|
||||
});
|
||||
await cardLinks.last().evaluate((link) => {
|
||||
(link as HTMLElement).focus();
|
||||
});
|
||||
await expect.poll(() => lastCardFullyVisible(rail)).toBe(true);
|
||||
|
||||
// A mouse press focuses the link too; the rail must not slide it
|
||||
// away from under the pointer before the click lands.
|
||||
await rail.evaluate((section) => {
|
||||
await rail.evaluate((section, selector) => {
|
||||
(document.activeElement as HTMLElement | null)?.blur();
|
||||
section
|
||||
.querySelector('.rail__track')
|
||||
.querySelector(selector)
|
||||
?.scrollTo({ left: 0, behavior: 'auto' });
|
||||
});
|
||||
}, hooks.track);
|
||||
await expect.poll(() => lastCardFullyVisible(rail)).toBe(false);
|
||||
const box = await rail
|
||||
.locator('.rail__card-link')
|
||||
.last()
|
||||
.boundingBox();
|
||||
const box = await cardLinks.last().boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
await app.mainWindow.mouse.move(
|
||||
(box?.x ?? 0) + 24,
|
||||
@@ -160,7 +161,7 @@ test.describe('Dashboard rail focus', () => {
|
||||
expect(await lastCardLinkFocused(app.mainWindow)).toBe(true);
|
||||
expect(
|
||||
await rail
|
||||
.locator('.rail__track')
|
||||
.locator(hooks.track)
|
||||
.evaluate((track) => track.scrollLeft)
|
||||
).toBe(0);
|
||||
await app.mainWindow.mouse.up();
|
||||
|
||||
@@ -158,11 +158,18 @@ and that pause holds the slide.
|
||||
hover lift are not clipped; the fades are offset by the same variable so
|
||||
they reach the track's clipping edge and no card strip shows beyond them.
|
||||
4. Cards are keyboard-focusable router links; `scroll-snap-align: start`
|
||||
means arrow-key nav lands on card boundaries.
|
||||
means arrow-key nav lands on card boundaries. A card that receives
|
||||
keyboard or script focus scrolls fully into the viewport: Chromium skips
|
||||
its own focus scroll once 32px of an element shows, so the track's
|
||||
`focusin` handler moves to the first card-start snap position revealing
|
||||
the whole card (a card wider than the viewport aligns at its own start).
|
||||
Focus caused by a press inside the track (within 100ms of `pointerdown`,
|
||||
650ms for touch) leaves the rail still, so the card does not slide from
|
||||
under the pointer before the click.
|
||||
5. Image handling: `loading="lazy"`, `decoding="async"`, fallback icon tile
|
||||
when `imageUrl` is missing or `error` fires.
|
||||
6. Dashboard hero, rail containers, rail cards, and "Manage all" links expose
|
||||
stable `data-test-id` hooks. Treat these as the supported Electron E2E
|
||||
6. Dashboard hero, rail containers, rail viewports and tracks, rail cards and
|
||||
their links, and "Manage all" links expose stable `data-test-id` hooks. Treat these as the supported Electron E2E
|
||||
selector surface; do not target internal CSS class names.
|
||||
|
||||
## Data Flow
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
}
|
||||
</header>
|
||||
|
||||
<div #viewport class="rail__viewport">
|
||||
<div
|
||||
#viewport
|
||||
class="rail__viewport"
|
||||
[attr.data-test-id]="testId() ? testId() + '-viewport' : null"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="rail__chev rail__chev--left"
|
||||
@@ -49,6 +53,7 @@
|
||||
#track
|
||||
class="rail__track"
|
||||
[attr.data-layout]="layout()"
|
||||
[attr.data-test-id]="testId() ? testId() + '-track' : null"
|
||||
(scroll)="onScroll()"
|
||||
(focusin)="onTrackFocusIn($event)"
|
||||
(pointerdown)="onTrackPointerDown($event)"
|
||||
@@ -69,6 +74,9 @@
|
||||
<a
|
||||
class="rail__channel-link"
|
||||
[routerLink]="card.link"
|
||||
[attr.data-test-id]="
|
||||
testId() ? testId() + '-card-link' : null
|
||||
"
|
||||
[queryParams]="card.queryParams ?? null"
|
||||
[state]="card.state"
|
||||
>
|
||||
@@ -155,6 +163,9 @@
|
||||
<a
|
||||
class="rail__card-link"
|
||||
[routerLink]="card.link"
|
||||
[attr.data-test-id]="
|
||||
testId() ? testId() + '-card-link' : null
|
||||
"
|
||||
[queryParams]="card.queryParams ?? null"
|
||||
[state]="card.state"
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user