mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
test(e2e): read sidebar categories in one snapshot inside polls (#1728)
The "allows restoring live categories after every category is hidden" Electron test failed intermittently on shard 1 across unrelated PRs and master. The app hid every category correctly (the failure screenshot shows "No categories available"), but readVisibleSidebarCategoryNames looped over count() with per-row nth(index) reads. When the sidebar removed a row between isVisible() and textContent(), textContent() auto-waited for the missing element, so the expect.poll predicate never returned and the poll timed out with "waiting on the predicate" instead of retrying. Read the visible rows with a single evaluateAll() snapshot in a shared sidebar-categories.e2e-support.ts helper, use it for the category picker in category-management and backup-roundtrip too, and record the rule in the validation map. Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4 files changed
+70
-77
No files matched your search
@@ -17,6 +17,7 @@ import {
|
||||
test,
|
||||
waitForXtreamWorkspaceReady,
|
||||
} from './electron-test-fixtures';
|
||||
import { readVisibleSidebarCategories } from './sidebar-categories.e2e-support';
|
||||
|
||||
/**
|
||||
* Full backup round-trip through the real UI, DB worker and IPC stack:
|
||||
@@ -237,32 +238,18 @@ async function pickVisibleCategoryWithContent(
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const categories = page.locator(
|
||||
'app-workspace-context-panel .category-item:visible'
|
||||
const category = (
|
||||
await readVisibleSidebarCategories(page)
|
||||
).find(
|
||||
(candidate) =>
|
||||
candidate.id &&
|
||||
candidate.name &&
|
||||
candidate.itemCount > 0
|
||||
);
|
||||
const count = await categories.count();
|
||||
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const category = categories.nth(index);
|
||||
const id =
|
||||
(
|
||||
await category.getAttribute('data-category-id')
|
||||
)?.trim() ?? '';
|
||||
const name =
|
||||
(
|
||||
await category
|
||||
.locator('.nav-item-label')
|
||||
.textContent()
|
||||
)?.trim() ?? '';
|
||||
const countText =
|
||||
(
|
||||
await category.locator('.item-count').textContent()
|
||||
)?.trim() ?? '';
|
||||
|
||||
if (id && name && (Number.parseInt(countText, 10) || 0) > 0) {
|
||||
picked = { id, name };
|
||||
return true;
|
||||
}
|
||||
if (category) {
|
||||
picked = { id: category.id, name: category.name };
|
||||
return true;
|
||||
}
|
||||
|
||||
picked = null;
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
waitForSourceRowIdle,
|
||||
waitForXtreamWorkspaceReady,
|
||||
} from './electron-test-fixtures';
|
||||
import {
|
||||
readVisibleSidebarCategories,
|
||||
readVisibleSidebarCategoryNames,
|
||||
} from './sidebar-categories.e2e-support';
|
||||
import { applyTheme } from './theme-contrast';
|
||||
|
||||
test.describe('Electron Xtream Category Management', () => {
|
||||
@@ -591,31 +595,6 @@ async function expectVisibleSidebarCategoryNames(
|
||||
}
|
||||
}
|
||||
|
||||
async function readVisibleSidebarCategoryNames(page: Page): Promise<string[]> {
|
||||
const categories = page.locator(
|
||||
'app-workspace-context-panel .category-item'
|
||||
);
|
||||
const actualNames: string[] = [];
|
||||
const count = await categories.count();
|
||||
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const category = categories.nth(index);
|
||||
if (!(await category.isVisible())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const categoryName =
|
||||
(await category.locator('.nav-item-label').textContent())?.trim() ??
|
||||
'';
|
||||
|
||||
if (categoryName) {
|
||||
actualNames.push(categoryName);
|
||||
}
|
||||
}
|
||||
|
||||
return actualNames;
|
||||
}
|
||||
|
||||
function stringArraysEqual(left: string[], right: string[]): boolean {
|
||||
return (
|
||||
left.length === right.length &&
|
||||
@@ -634,35 +613,12 @@ async function pickSidebarCategory(
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const categories = page.locator(
|
||||
'app-workspace-context-panel .category-item:visible'
|
||||
const candidates = (
|
||||
await readVisibleSidebarCategories(page)
|
||||
).filter(
|
||||
(candidate) =>
|
||||
candidate.id && candidate.name && candidate.itemCount > 0
|
||||
);
|
||||
const count = await categories.count();
|
||||
const candidates: Array<{
|
||||
id: string;
|
||||
itemCount: number;
|
||||
name: string;
|
||||
}> = [];
|
||||
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const category = categories.nth(index);
|
||||
const id =
|
||||
(await category.getAttribute('data-category-id'))?.trim() ??
|
||||
'';
|
||||
const name =
|
||||
(
|
||||
await category.locator('.nav-item-label').textContent()
|
||||
)?.trim() ?? '';
|
||||
const countText =
|
||||
(
|
||||
await category.locator('.item-count').textContent()
|
||||
)?.trim() ?? '';
|
||||
const itemCount = Number.parseInt(countText, 10) || 0;
|
||||
|
||||
if (id && name && itemCount > 0) {
|
||||
candidates.push({ id, itemCount, name });
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
preferredCandidate = null;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export interface SidebarCategorySnapshot {
|
||||
id: string;
|
||||
itemCount: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const VISIBLE_SIDEBAR_CATEGORIES =
|
||||
'app-workspace-context-panel .category-item:visible';
|
||||
|
||||
/**
|
||||
* Reads the visible sidebar categories in one DOM snapshot.
|
||||
*
|
||||
* Do not loop over `count()` with per-row `nth(index)` reads here: those
|
||||
* locator calls auto-wait, so a row the sidebar removes between the count and
|
||||
* the read (for example right after hiding categories) blocks the call until
|
||||
* the surrounding `expect.poll` times out instead of letting the poll retry.
|
||||
*/
|
||||
export async function readVisibleSidebarCategories(
|
||||
page: Page
|
||||
): Promise<SidebarCategorySnapshot[]> {
|
||||
return page.locator(VISIBLE_SIDEBAR_CATEGORIES).evaluateAll((rows) =>
|
||||
rows.map((row) => ({
|
||||
id: row.getAttribute('data-category-id')?.trim() ?? '',
|
||||
itemCount:
|
||||
Number.parseInt(
|
||||
row.querySelector('.item-count')?.textContent?.trim() ?? '',
|
||||
10
|
||||
) || 0,
|
||||
name:
|
||||
row.querySelector('.nav-item-label')?.textContent?.trim() ?? '',
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
export async function readVisibleSidebarCategoryNames(
|
||||
page: Page
|
||||
): Promise<string[]> {
|
||||
return (await readVisibleSidebarCategories(page))
|
||||
.map((category) => category.name)
|
||||
.filter(Boolean);
|
||||
}
|
||||
@@ -181,6 +181,13 @@ and JSON summary output. CI uploads the merged Tier A report to Codecov with the
|
||||
Use atomized E2E targets when available, for example
|
||||
`pnpm nx run web-e2e:e2e-ci--src/xtream.e2e.ts`.
|
||||
|
||||
Inside `expect.poll`, read a changing list in one DOM snapshot
|
||||
(`allTextContents()` or `evaluateAll()`, as in
|
||||
`apps/electron-backend-e2e/src/sidebar-categories.e2e-support.ts`), not by
|
||||
looping over `count()` with per-row `nth(index)` reads. Those reads auto-wait,
|
||||
so a row removed mid-loop hangs the predicate until the poll times out instead
|
||||
of letting it retry.
|
||||
|
||||
Playwright coverage is measured semantically by tags and critical journeys, not
|
||||
by a source-line percentage. E2E reports should use tags such as `@critical`,
|
||||
`@electron`, `@web`, `@xtream`, `@stalker`, `@m3u`, `@search`, `@epg`,
|
||||
|
||||
Reference in new issue
Block a user