Files
iptvnator/apps/electron-backend-e2e/src/recent.e2e.ts
T
4grayandClaude Fable 5.1 c76a901e8d fix(ui): keep one Back arrow on detail pages (#1627)
Movie and series detail pages showed two arrow_back controls while the
inline player was open: the shell's sticky arrow (added in #1576 so Back
survives scrolling) meant "Close player" in watch state, while the
now-playing bar carried a second arrow that meant route-level Back — the
same icon with two meanings, next to a "Close player" button that
duplicated the first.

The shell's sticky arrow is now route-level Back in browse and watch
alike, and the bar carries no arrow of its own. Closing the player is
the bar's "Close player" button and Escape, which still unwinds one
level (close, then back). Hosts without a browse Back target (M3U,
downloads) render no arrow in either state.

Unit specs for the shell and the inline player cover the new contract;
the Electron and web E2E helpers that pressed Back from watch are
updated, and the M3U flow closes the player through the bar's button.
Docs, the mirrored CLAUDE.md/AGENTS.md paragraph and a release note
follow the change.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 10:44:17 +02:00

753 lines
29 KiB
TypeScript

import { Page } from '@playwright/test';
import {
addStalkerPortal,
addXtreamPortal,
channelItemByTitle,
clickCategoryById,
clickGridListCardByTitle,
clickCategoryByNameExact,
clickFirstGridListCard,
closeElectronApp,
contentCardByTitle,
defaultXtreamPassword,
defaultXtreamUsername,
expectPathname,
expectVisibleContentCardTitle,
expect,
importM3uPlaylistFromNativeDialog,
launchElectronApp,
openPlaylistFavorites,
openPlaylistRecent,
openWorkspaceSection,
openSources,
resetMockServers,
restartElectronApp,
sourceRowByTitle,
switchUnifiedCollectionContent,
switchUnifiedCollectionScope,
test,
playlistSwitcherTitle,
waitForM3uCatalog,
waitForStalkerCatalog,
waitForXtreamWorkspaceReady,
writeTemporaryM3uFile,
} from './electron-test-fixtures';
import {
fetchStalkerCategoryFixture,
fetchXtreamLiveFixture,
fetchXtreamSeriesFixture,
fetchXtreamVodFixture,
getStalkerTitle,
getXtreamTitle,
pickDistinctTitles,
} from './portal-mock-fixtures';
test.describe('Electron Recently Viewed', () => {
test('keeps unified live detail open when re-clicking the active M3U recent item', async ({
dataDir,
}) => {
const filePath = writeTemporaryM3uFile(
dataDir,
'm3u-live-reclick.m3u',
[
{
groupTitle: 'Live',
name: 'Stable Recent Channel',
url: 'https://streams.example.test/stable-recent.m3u8',
},
]
);
const app = await launchElectronApp(dataDir);
try {
await importM3uPlaylistFromNativeDialog(app, filePath);
await waitForM3uCatalog(app.mainWindow);
await channelItemByTitle(app.mainWindow, 'Stable Recent Channel')
.first()
.click();
await openPlaylistRecent(app.mainWindow);
const item = channelItemByTitle(
app.mainWindow,
'Stable Recent Channel'
).first();
const livePlayer = app.mainWindow
.locator(
'app-unified-live-tab .content-container .video-player'
)
.first();
await item.click();
await expect(livePlayer).toBeVisible({ timeout: 20000 });
await expect(item).toHaveClass(/(^|\s)active(\s|$)/);
await item.click();
await expect(livePlayer).toBeVisible({ timeout: 20000 });
await expect(item).toHaveClass(/(^|\s)active(\s|$)/);
} finally {
await closeElectronApp(app);
}
});
test('@persistence @m3u @electron tracks M3U recent channels in newest-first order, supports all-playlists scope, and persists favorites after restart', async ({
dataDir,
}) => {
const playlistTitle = 'm3u-recent-source.m3u';
const playlistSourceTitle = 'm3u-recent-source';
const filePath = writeTemporaryM3uFile(dataDir, playlistTitle, [
{
groupTitle: 'Live',
name: 'Recent Channel One',
url: 'https://streams.example.test/recent-one.m3u8',
},
{
groupTitle: 'Live',
name: 'Recent Channel Two',
url: 'https://streams.example.test/recent-two.m3u8',
},
]);
const app = await launchElectronApp(dataDir);
try {
await importM3uPlaylistFromNativeDialog(app, filePath);
await waitForM3uCatalog(app.mainWindow);
await channelItemByTitle(app.mainWindow, 'Recent Channel One')
.first()
.click();
await channelItemByTitle(app.mainWindow, 'Recent Channel Two')
.first()
.click();
await openPlaylistRecent(app.mainWindow);
await expect
.poll(() => visibleLiveTitles(app.mainWindow))
.toEqual(['Recent Channel Two', 'Recent Channel One']);
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
await expect
.poll(() => visibleLiveTitles(app.mainWindow))
.toEqual(['Recent Channel Two', 'Recent Channel One']);
await toggleFavoriteForChannel(
app.mainWindow,
'Recent Channel Two'
);
await expect
.poll(() => visibleLiveTitles(app.mainWindow))
.toEqual(['Recent Channel Two', 'Recent Channel One']);
await openPlaylistFavorites(app.mainWindow);
await expect(
channelItemByTitle(app.mainWindow, 'Recent Channel Two').first()
).toBeVisible({ timeout: 20000 });
const restarted = await restartElectronApp(app, dataDir);
app.electronApp = restarted.electronApp;
app.mainWindow = restarted.mainWindow;
await openSources(app.mainWindow);
await sourceRowByTitle(app.mainWindow, playlistSourceTitle)
.first()
.click();
await waitForM3uCatalog(app.mainWindow);
await openPlaylistRecent(app.mainWindow);
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
await expect
.poll(() => visibleLiveTitles(app.mainWindow))
.toEqual(['Recent Channel Two', 'Recent Channel One']);
await openPlaylistFavorites(app.mainWindow);
await expect(
channelItemByTitle(app.mainWindow, 'Recent Channel Two').first()
).toBeVisible({ timeout: 20000 });
} finally {
await closeElectronApp(app);
}
});
test('removes one M3U recent live channel from the all-playlists context menu', async ({
dataDir,
}) => {
const filePath = writeTemporaryM3uFile(
dataDir,
'm3u-recent-context-menu-source.m3u',
[
{
groupTitle: 'Live',
name: 'Context Recent One',
url: 'https://streams.example.test/context-recent-one.m3u8',
},
{
groupTitle: 'Live',
name: 'Context Recent Two',
url: 'https://streams.example.test/context-recent-two.m3u8',
},
]
);
const app = await launchElectronApp(dataDir);
try {
await importM3uPlaylistFromNativeDialog(app, filePath);
await waitForM3uCatalog(app.mainWindow);
await channelItemByTitle(app.mainWindow, 'Context Recent One')
.first()
.click();
await channelItemByTitle(app.mainWindow, 'Context Recent Two')
.first()
.click();
await openPlaylistRecent(app.mainWindow);
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
await expect
.poll(() => visibleLiveTitles(app.mainWindow))
.toEqual(['Context Recent Two', 'Context Recent One']);
await channelItemByTitle(app.mainWindow, 'Context Recent One')
.first()
.click({ button: 'right' });
await app.mainWindow
.getByRole('menuitem', { name: 'Remove from history' })
.click();
await expect
.poll(() => visibleLiveTitles(app.mainWindow))
.toEqual(['Context Recent Two']);
await expect(
channelItemByTitle(app.mainWindow, 'Context Recent One')
).toHaveCount(0);
} finally {
await closeElectronApp(app);
}
});
test('@persistence @xtream @electron tracks Xtream live, movie, and series history across playlist and all-playlists scope, persists after restart, and supports clearing', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['xtream']);
const liveFixture = await fetchXtreamLiveFixture(
request,
xtreamCredentials
);
const vodFixture = await fetchXtreamVodFixture(
request,
xtreamCredentials
);
const seriesFixture = await fetchXtreamSeriesFixture(
request,
xtreamCredentials
);
const [liveTitle] = pickDistinctTitles(
liveFixture.items,
getXtreamTitle
);
const portalTitle = 'Xtream Recent Source';
const app = await launchElectronApp(dataDir);
try {
await addXtreamPortal(app.mainWindow, {
name: portalTitle,
});
await waitForXtreamWorkspaceReady(app.mainWindow);
await openWorkspaceSection(app.mainWindow, 'Live TV');
await clickCategoryByNameExact(
app.mainWindow,
liveFixture.categoryName
);
await toggleFavoriteForChannel(app.mainWindow, liveTitle);
await openPlaylistFavorites(app.mainWindow);
await channelItemByTitle(app.mainWindow, liveTitle).first().click();
await expectUnifiedLiveDetailOpen(app.mainWindow, liveTitle);
await app.mainWindow
.getByRole('link', { name: 'Movies', exact: true })
.click();
await clickCategoryByNameExact(
app.mainWindow,
vodFixture.categoryName
);
// Pick from the displayed grid: fixture order ≠ display order (date-desc
// sort + pagination), so the first fixture item may not be on page 1.
// 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);
await app.mainWindow
.getByRole('link', { name: 'Series', exact: true })
.click();
await clickCategoryByNameExact(
app.mainWindow,
seriesFixture.categoryName
);
const seriesTitle = await clickFirstGridListCard(app.mainWindow);
await playFirstSeriesEpisode(app.mainWindow);
await openPlaylistRecent(app.mainWindow);
await expect(
channelItemByTitle(app.mainWindow, liveTitle)
).toHaveCount(1);
await switchUnifiedCollectionContent(app.mainWindow, 'Movies');
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
await expect(
channelItemByTitle(app.mainWindow, liveTitle)
).toHaveCount(1);
await switchUnifiedCollectionContent(app.mainWindow, 'Movies');
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
const restarted = await restartElectronApp(app, dataDir);
app.electronApp = restarted.electronApp;
app.mainWindow = restarted.mainWindow;
await openSources(app.mainWindow);
await sourceRowByTitle(app.mainWindow, portalTitle).first().click();
await waitForXtreamWorkspaceReady(app.mainWindow);
await openPlaylistRecent(app.mainWindow);
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
await expect(
channelItemByTitle(app.mainWindow, liveTitle)
).toHaveCount(1);
await switchUnifiedCollectionContent(app.mainWindow, 'Movies');
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
await clearRecentItems(app.mainWindow, 'Live TV');
await expect(
channelItemByTitle(app.mainWindow, liveTitle)
).toHaveCount(0);
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
await clearRecentItems(app.mainWindow, 'Movies');
await expect(
contentCardByTitle(app.mainWindow, movieTitle)
).toHaveCount(0);
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
await clearRecentItems(app.mainWindow, 'Series');
await expect(
contentCardByTitle(app.mainWindow, seriesTitle)
).toHaveCount(0);
} finally {
await closeElectronApp(app);
}
});
test('opens Xtream recent movies and series inline from recent without switching the playlist or showing the sidebar', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['xtream']);
const vodFixture = await fetchXtreamVodFixture(
request,
xtreamCredentials
);
const seriesFixture = await fetchXtreamSeriesFixture(
request,
xtreamCredentials
);
const portalTitle = 'Xtream Recent Detail Source';
const app = await launchElectronApp(dataDir);
try {
await addXtreamPortal(app.mainWindow, {
name: portalTitle,
});
await waitForXtreamWorkspaceReady(app.mainWindow);
await app.mainWindow
.getByRole('link', { name: 'Movies', exact: true })
.click();
await clickCategoryByNameExact(
app.mainWindow,
vodFixture.categoryName
);
const movieTitle = await clickFirstGridListCard(app.mainWindow);
await playCurrentDetail(app.mainWindow);
await goBackFromDetail(app.mainWindow);
await app.mainWindow
.getByRole('link', { name: 'Series', exact: true })
.click();
await clickCategoryByNameExact(
app.mainWindow,
seriesFixture.categoryName
);
const seriesTitle = await clickFirstGridListCard(app.mainWindow);
await playFirstSeriesEpisode(app.mainWindow);
await openPlaylistRecent(app.mainWindow);
await expect(playlistSwitcherTitle(app.mainWindow)).toContainText(
portalTitle
);
await switchUnifiedCollectionContent(app.mainWindow, 'Movies');
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
await contentCardByTitle(app.mainWindow, movieTitle)
.first()
.click();
await expectInlineCollectionDetail(app.mainWindow, {
pathname: /\/workspace\/global-recent$/,
title: movieTitle,
playlistTitle: portalTitle,
});
await goBackFromDetail(app.mainWindow);
await expectPathname(app.mainWindow, /\/workspace\/global-recent$/);
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
await contentCardByTitle(app.mainWindow, seriesTitle)
.first()
.click();
await expectInlineCollectionDetail(app.mainWindow, {
pathname: /\/workspace\/global-recent$/,
title: seriesTitle,
playlistTitle: portalTitle,
});
await goBackFromDetail(app.mainWindow);
await expectPathname(app.mainWindow, /\/workspace\/global-recent$/);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
} finally {
await closeElectronApp(app);
}
});
test('@persistence @stalker @electron tracks Stalker live, movie, and series history across playlist and all-playlists scope, and preserves it after restart', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['stalker']);
const liveFixture = await fetchStalkerCategoryFixture(request, 'itv');
const vodFixture = await fetchStalkerCategoryFixture(request, 'vod');
const seriesFixture = await fetchStalkerCategoryFixture(
request,
'series'
);
const [liveTitle] = pickDistinctTitles(
liveFixture.items,
getStalkerTitle
);
const [movieTitle] = pickDistinctTitles(
vodFixture.items,
getStalkerTitle
);
const [seriesTitle] = pickDistinctTitles(
seriesFixture.items,
getStalkerTitle
);
const portalTitle = 'Stalker Recent Source';
const app = await launchElectronApp(dataDir);
try {
await addStalkerPortal(app.mainWindow, {
name: portalTitle,
});
await waitForStalkerCatalog(app.mainWindow);
await app.mainWindow
.getByRole('link', { name: 'Live TV', exact: true })
.click();
await clickCategoryById(app.mainWindow, liveFixture.categoryId);
await toggleFavoriteForChannel(app.mainWindow, liveTitle);
await openPlaylistFavorites(app.mainWindow);
await channelItemByTitle(app.mainWindow, liveTitle).first().click();
await expectUnifiedLiveDetailOpen(app.mainWindow, liveTitle);
await app.mainWindow
.getByRole('link', { name: 'Movies', exact: true })
.click();
await clickCategoryById(app.mainWindow, vodFixture.categoryId);
await clickGridListCardByTitle(app.mainWindow, movieTitle);
await playCurrentDetail(app.mainWindow);
await goBackFromDetail(app.mainWindow);
await app.mainWindow
.getByRole('link', { name: 'Series', exact: true })
.click();
await clickCategoryById(app.mainWindow, seriesFixture.categoryId);
await clickGridListCardByTitle(app.mainWindow, seriesTitle);
await playFirstSeriesEpisode(app.mainWindow);
await openPlaylistRecent(app.mainWindow);
await expect(
channelItemByTitle(app.mainWindow, liveTitle)
).toHaveCount(1);
await switchUnifiedCollectionContent(app.mainWindow, 'Movies');
await expect(
contentCardByTitle(app.mainWindow, movieTitle)
).toHaveCount(1);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expect(
contentCardByTitle(app.mainWindow, seriesTitle)
).toHaveCount(1);
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
await expect(
channelItemByTitle(app.mainWindow, liveTitle)
).toHaveCount(1);
await switchUnifiedCollectionContent(app.mainWindow, 'Movies');
await expect(
contentCardByTitle(app.mainWindow, movieTitle)
).toHaveCount(1);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expect(
contentCardByTitle(app.mainWindow, seriesTitle)
).toHaveCount(1);
const restarted = await restartElectronApp(app, dataDir);
app.electronApp = restarted.electronApp;
app.mainWindow = restarted.mainWindow;
await openSources(app.mainWindow);
await sourceRowByTitle(app.mainWindow, portalTitle).first().click();
await waitForStalkerCatalog(app.mainWindow);
await openPlaylistRecent(app.mainWindow);
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
await expect(
channelItemByTitle(app.mainWindow, liveTitle)
).toHaveCount(1);
await switchUnifiedCollectionContent(app.mainWindow, 'Movies');
await expect(
contentCardByTitle(app.mainWindow, movieTitle)
).toHaveCount(1);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expect(
contentCardByTitle(app.mainWindow, seriesTitle)
).toHaveCount(1);
} finally {
await closeElectronApp(app);
}
});
test('opens Stalker recent movies and series inline from recent without showing the sidebar', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['stalker']);
const vodFixture = await fetchStalkerCategoryFixture(request, 'vod');
const seriesFixture = await fetchStalkerCategoryFixture(
request,
'series'
);
const [movieTitle] = pickDistinctTitles(
vodFixture.items,
getStalkerTitle
);
const [seriesTitle] = pickDistinctTitles(
seriesFixture.items,
getStalkerTitle
);
const portalTitle = 'Stalker Recent Detail Source';
const app = await launchElectronApp(dataDir);
try {
await addStalkerPortal(app.mainWindow, {
name: portalTitle,
});
await waitForStalkerCatalog(app.mainWindow);
await app.mainWindow
.getByRole('link', { name: 'Movies', exact: true })
.click();
await clickCategoryById(app.mainWindow, vodFixture.categoryId);
await clickGridListCardByTitle(app.mainWindow, movieTitle);
await playCurrentDetail(app.mainWindow);
await goBackFromDetail(app.mainWindow);
await app.mainWindow
.getByRole('link', { name: 'Series', exact: true })
.click();
await clickCategoryById(app.mainWindow, seriesFixture.categoryId);
await clickGridListCardByTitle(app.mainWindow, seriesTitle);
await playFirstSeriesEpisode(app.mainWindow);
await openPlaylistRecent(app.mainWindow);
await expect(playlistSwitcherTitle(app.mainWindow)).toContainText(
portalTitle
);
await switchUnifiedCollectionContent(app.mainWindow, 'Movies');
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
await contentCardByTitle(app.mainWindow, movieTitle)
.first()
.click();
await expectInlineCollectionDetail(app.mainWindow, {
pathname: /\/workspace\/global-recent$/,
title: movieTitle,
playlistTitle: portalTitle,
});
await playCurrentDetail(app.mainWindow);
await expectInlinePlayerWithoutDialog(app.mainWindow);
await goBackFromDetail(app.mainWindow);
await expectPathname(app.mainWindow, /\/workspace\/global-recent$/);
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
await contentCardByTitle(app.mainWindow, seriesTitle)
.first()
.click();
await expectInlineCollectionDetail(app.mainWindow, {
pathname: /\/workspace\/global-recent$/,
title: seriesTitle,
playlistTitle: portalTitle,
});
await playFirstSeriesEpisode(app.mainWindow);
await expectInlinePlayerWithoutDialog(app.mainWindow);
await goBackFromDetail(app.mainWindow);
await expectPathname(app.mainWindow, /\/workspace\/global-recent$/);
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
} finally {
await closeElectronApp(app);
}
});
});
const xtreamCredentials = {
username: defaultXtreamUsername,
password: defaultXtreamPassword,
};
async function clearRecentItems(page: Page, typeLabel: string): Promise<void> {
await page
.getByRole('button', { name: `Clear recently viewed ${typeLabel}` })
.click();
await page.getByRole('button', { name: 'Yes' }).click();
}
async function expectUnifiedLiveDetailOpen(
page: Page,
title: string
): Promise<void> {
await expect(
page
.locator('app-unified-live-tab .content-container .video-player')
.first()
).toBeVisible({ timeout: 20000 });
await expect(channelItemByTitle(page, title).first()).toHaveClass(
/(^|\s)active(\s|$)/
);
}
async function goBackFromDetail(page: Page): Promise<void> {
// Return to the list: the shell's sticky Back is route-level in browse
// and watch alike (closing the player is the bar's own Close button).
const backButton = page
.locator('app-portal-detail-shell')
.first()
.getByRole('button', { name: 'Back', exact: true });
await expect(backButton).toBeVisible({ timeout: 20000 });
try {
await backButton.click({ timeout: 5000 });
} catch {
await backButton.evaluate((button: HTMLButtonElement) =>
button.click()
);
}
}
async function expectInlineCollectionDetail(
page: Page,
params: {
pathname: RegExp;
title: string;
playlistTitle: string;
}
): Promise<void> {
await expectPathname(page, params.pathname);
await expect(playlistSwitcherTitle(page)).toContainText(
params.playlistTitle
);
await expect(page.locator('app-workspace-context-panel')).toHaveCount(0);
await expect(page.locator('app-content-hero')).toContainText(params.title);
await expect(
page.locator('app-portal-detail-shell .shell__back-button').first()
).toBeVisible({ timeout: 20000 });
}
async function expectInlinePlayerWithoutDialog(page: Page): Promise<void> {
await expect(
page.locator('app-portal-inline-player app-web-player-view').first()
).toBeVisible({ timeout: 20000 });
await expect(
page.locator('mat-dialog-container app-web-player-view')
).toHaveCount(0);
}
async function playCurrentDetail(page: Page): Promise<void> {
const playButton = page.locator('button.play-btn').first();
await expect(playButton).toBeVisible({ timeout: 20000 });
await playButton.click();
}
async function playFirstSeriesEpisode(page: Page): Promise<void> {
const seasonCard = page.locator('.season-card').first();
const episodeCard = page
.locator('.episode-card, .episode-list-item')
.first();
await expect
.poll(
async () =>
(await seasonCard.count()) + (await episodeCard.count()),
{ timeout: 20000 }
)
.toBeGreaterThan(0);
if ((await seasonCard.count()) > 0) {
await seasonCard.scrollIntoViewIfNeeded();
await expect(seasonCard).toBeVisible({ timeout: 20000 });
await seasonCard.click();
}
await episodeCard.scrollIntoViewIfNeeded();
await expect(episodeCard).toBeVisible({ timeout: 20000 });
await episodeCard.click();
}
async function toggleFavoriteForChannel(
page: Page,
title: string
): Promise<void> {
const item = channelItemByTitle(page, title).first();
await expect(item).toBeVisible({ timeout: 20000 });
await item.hover();
await item.locator('.favorite-button').first().click();
await expect(item.locator('.favorite-button mat-icon').first()).toHaveText(
'star'
);
}
async function visibleLiveTitles(page: Page): Promise<string[]> {
return page
.locator('[data-test-id="channel-item"] .channel-name')
.allInnerTexts()
.then((titles) => titles.map((title) => title.trim()));
}