fix(e2e): fix category-management navigation race and grid card click race

Two separate root causes:

1. category-management.e2e.ts: openWorkspaceSection('Live TV') was not
   awaited — pickSidebarCategory ran before the route settled on the live
   section, so it read VOD categories from the stale sidebar (e.g. 'Action'
   instead of a live category). The manage-categories dialog opens for the
   live section, so searching for a VOD name found nothing.
   Fix: add waitForURL(/\/workspace\/xtreams\/[^/]+\/live/) after both
   openWorkspaceSection('Live TV') calls.

2. recent.e2e.ts / favorites.e2e.ts: waitForFirstGridListCardTitle read
   the first card's title, but clickGridListCardByTitle then did a fresh
   DOM search by that title. Between those two calls the grid could
   re-sort/paginate, making the card unfindable.
   Fix: new clickFirstGridListCard helper atomically reads the title and
   clicks the same card reference, replacing the two-step pattern.

https://claude.ai/code/session_01VU6ZZTG7YrhKhKBQvBfmpp
This commit is contained in:
Claude committed 2026-04-01 06:07:26 +00:00
1 parent df0c7f79d1
commit 0f3a75f2b2
4 files changed
+41 -14

No files matched your search

@@ -30,6 +30,12 @@ test.describe('Electron Xtream Category Management', () => {
});
await waitForXtreamWorkspaceReady(app.mainWindow);
await openWorkspaceSection(app.mainWindow, 'Live TV');
// Wait for the route to settle on the live TV section so that
// the sidebar shows live categories (not VOD/series from a
// previous section) before we read from it.
await app.mainWindow.waitForURL(
/\/workspace\/xtreams\/[^/]+\/live/
);
const targetCategory = await pickSidebarCategory(app.mainWindow);
@@ -73,6 +79,9 @@ test.describe('Electron Xtream Category Management', () => {
await sourceRowByTitle(app.mainWindow, portalName).first().click();
await waitForXtreamWorkspaceReady(app.mainWindow);
await openWorkspaceSection(app.mainWindow, 'Live TV');
await app.mainWindow.waitForURL(
/\/workspace\/xtreams\/[^/]+\/live/
);
await expect(sidebarCategoryById(app.mainWindow, targetCategory.id)).toHaveCount(
0
);
@@ -635,6 +635,25 @@ export async function waitForFirstGridListCardTitle(page: Page): Promise<string>
return ((await card.locator('.title').textContent()) ?? '').trim();
}
/**
* Waits for the first grid card to appear with a non-empty title, clicks it,
* and returns the title. Use this instead of waitForFirstGridListCardTitle +
* clickGridListCardByTitle to avoid a race condition where the grid re-renders
* between the title read and the subsequent search-by-title click.
*/
export async function clickFirstGridListCard(page: Page): Promise<string> {
const card = page.locator('.category-content-layout mat-card').first();
await expect(card).toBeVisible({ timeout: 20000 });
const titleEl = card.locator('.title');
let title = '';
await expect(async () => {
title = ((await titleEl.textContent()) ?? '').trim();
expect(title.length).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
await card.click();
return title;
}
export async function clickGridListCardByTitle(
page: Page,
title: string
@@ -5,7 +5,7 @@ import {
channelItemByTitle,
clickCategoryById,
clickCategoryByNameExact,
clickGridListCardByTitle,
clickFirstGridListCard,
closeElectronApp,
contentCardByTitle,
defaultXtreamPassword,
@@ -22,7 +22,6 @@ import {
switchUnifiedCollectionContent,
switchUnifiedCollectionScope,
test,
waitForFirstGridListCardTitle,
waitForM3uCatalog,
waitForStalkerCatalog,
waitForXtreamWorkspaceReady,
@@ -138,10 +137,11 @@ test.describe('Electron Favorites', () => {
app.mainWindow,
vodFixture.categoryName
);
// Pick the title from the first displayed card (grid sorts by date-desc,
// so fixture order ≠ display order; pagination may hide some items).
const movieTitle = await waitForFirstGridListCardTitle(app.mainWindow);
await clickGridListCardByTitle(app.mainWindow, movieTitle);
// Use clickFirstGridListCard: grid sorts by date-desc so fixture order ≠
// display order; this atomically reads the first card's title and clicks
// it, avoiding a race where the grid re-renders between the title read
// and a separate search-by-title click.
const movieTitle = await clickFirstGridListCard(app.mainWindow);
await addCurrentDetailToFavorites(app.mainWindow);
await goBackFromDetail(app.mainWindow);
@@ -150,8 +150,7 @@ test.describe('Electron Favorites', () => {
app.mainWindow,
seriesFixture.categoryName
);
const seriesTitle = await waitForFirstGridListCardTitle(app.mainWindow);
await clickGridListCardByTitle(app.mainWindow, seriesTitle);
const seriesTitle = await clickFirstGridListCard(app.mainWindow);
await addCurrentDetailToFavorites(app.mainWindow);
await goBackFromDetail(app.mainWindow);
+6 -6
View File
@@ -5,7 +5,7 @@ import {
channelItemByTitle,
clickCategoryById,
clickCategoryByNameExact,
clickGridListCardByTitle,
clickFirstGridListCard,
closeElectronApp,
contentCardByTitle,
defaultXtreamPassword,
@@ -23,7 +23,6 @@ import {
switchUnifiedCollectionContent,
switchUnifiedCollectionScope,
test,
waitForFirstGridListCardTitle,
waitForM3uCatalog,
waitForStalkerCatalog,
waitForXtreamWorkspaceReady,
@@ -142,8 +141,10 @@ test.describe('Electron Recently Viewed', () => {
);
// Pick from the displayed grid: fixture order ≠ display order (date-desc
// sort + pagination), so the first fixture item may not be on page 1.
const movieTitle = await waitForFirstGridListCardTitle(app.mainWindow);
await clickGridListCardByTitle(app.mainWindow, movieTitle);
// Use clickFirstGridListCard to atomically read + click the first visible
// card, avoiding a race where the grid re-renders between title read and
// a separate search-by-title click.
const movieTitle = await clickFirstGridListCard(app.mainWindow);
await playCurrentDetail(app.mainWindow);
await goBackFromDetail(app.mainWindow);
@@ -152,8 +153,7 @@ test.describe('Electron Recently Viewed', () => {
app.mainWindow,
seriesFixture.categoryName
);
const seriesTitle = await waitForFirstGridListCardTitle(app.mainWindow);
await clickGridListCardByTitle(app.mainWindow, seriesTitle);
const seriesTitle = await clickFirstGridListCard(app.mainWindow);
await playFirstSeriesEpisode(app.mainWindow);
await openPlaylistRecent(app.mainWindow);