mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
feat(portals): cinematic series details with the episodes grid in the first screen
Series pages take the same hero as movies: the provider's season marker moves out of the title into a "Season N" chip, the status chip is localized, the primary reads Continue / "S02E03 · 18m left" or Play / the next episode, Trailer opens the modal, and favorites plus a "…" menu complete the row. The menu carries mark season/series watched, reset progress, open in external player, copy the next episode's URL, download season, show in category and hide from Continue Watching — rows a provider cannot serve are left out. The season container's header is "Episodes" with the season pills and count; the season actions it used to show live in the menu; episode cards are flat with a watched bar on the artwork, the plot and "42 min · 18m left". Cast & crew and Similar follow the episodes. The Electron and web E2E flows and the season container specs drive the new menu and presenters; legacy detail styles are removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
075c056ba1
commit
c2f5d67120
42 files changed
+1969
-1893
No files matched your search
@@ -244,7 +244,9 @@ export async function expectSeriesSurfacesInBothThemes(
|
||||
const shell = page.locator('app-portal-detail-shell');
|
||||
await expect
|
||||
.poll(() =>
|
||||
rasterizedBorderContrast(shell.locator('.favorite-btn').first())
|
||||
rasterizedBorderContrast(
|
||||
shell.locator('[data-testid="series-favorite-toggle"]').first()
|
||||
)
|
||||
)
|
||||
.toBeGreaterThan(1.1);
|
||||
for (const selector of ['.episode-card', 'mat-button-toggle-group']) {
|
||||
|
||||
@@ -147,6 +147,7 @@ export async function verifyStalkerSeasonMarkers(
|
||||
'src',
|
||||
new RegExp(`/season-${season}\\.jpg$`)
|
||||
);
|
||||
await page.locator('[data-testid="series-more-menu"]').click();
|
||||
await page.locator('[data-test-id="toggle-season-watched"]').click();
|
||||
await expect(page.locator('.episode-card--watched')).toHaveCount(1);
|
||||
await page.getByRole('button', { name: 'Back', exact: true }).click();
|
||||
|
||||
@@ -1104,10 +1104,9 @@ test('@stalker season watched toggle — embedded series marks and clears every
|
||||
|
||||
await expectSeriesSurfacesInBothThemes(page, testInfo);
|
||||
|
||||
// The season header's bulk toggle (data-test-id with a dash — getByTestId
|
||||
// The hero menu's bulk toggle (data-test-id with a dash — getByTestId
|
||||
// only matches data-testid in this suite) counts every unwatched episode.
|
||||
const seasonToggle = page.locator('[data-test-id="toggle-season-watched"]');
|
||||
await expect(seasonToggle).toBeVisible();
|
||||
let seasonToggle = await seriesMenuRow(page, 'toggle-season-watched');
|
||||
await expect(seasonToggle).toContainText(
|
||||
`Mark season as watched (${episodeCount})`
|
||||
);
|
||||
@@ -1116,27 +1115,44 @@ test('@stalker season watched toggle — embedded series marks and clears every
|
||||
|
||||
await seasonToggle.click();
|
||||
|
||||
// All episodes get full-progress positions: the button flips and every
|
||||
// All episodes get full-progress positions: the row flips and every
|
||||
// episode card plus per-episode toggle shows the watched state.
|
||||
await expect(seasonToggle).toContainText('Mark season as unwatched', {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(watchedCards).toHaveCount(episodeCount);
|
||||
await expect(watchedCards).toHaveCount(episodeCount, { timeout: 15_000 });
|
||||
await expect(
|
||||
page.locator(
|
||||
'[data-testid="episode-watched-toggle"].episode-card__watched-toggle--watched'
|
||||
)
|
||||
).toHaveCount(episodeCount);
|
||||
seasonToggle = await seriesMenuRow(page, 'toggle-season-watched');
|
||||
await expect(seasonToggle).toContainText('Mark season as unwatched');
|
||||
|
||||
// Second click clears every episode's position again.
|
||||
await seasonToggle.click();
|
||||
await expect(watchedCards).toHaveCount(0, { timeout: 15_000 });
|
||||
seasonToggle = await seriesMenuRow(page, 'toggle-season-watched');
|
||||
await expect(seasonToggle).toContainText(
|
||||
`Mark season as watched (${episodeCount})`,
|
||||
{ timeout: 15_000 }
|
||||
`Mark season as watched (${episodeCount})`
|
||||
);
|
||||
await expect(watchedCards).toHaveCount(0);
|
||||
await closeSeriesMenu(page);
|
||||
});
|
||||
|
||||
|
||||
/** Opens the series "…" menu (if closed) and returns the row with that test id. */
|
||||
async function seriesMenuRow(page: Page, rowTestId: string) {
|
||||
const row = page.locator(`[data-test-id="${rowTestId}"]`);
|
||||
if (!(await row.isVisible().catch(() => false))) {
|
||||
await page.locator('[data-testid="series-more-menu"]').click();
|
||||
}
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
return row;
|
||||
}
|
||||
|
||||
async function closeSeriesMenu(page: Page): Promise<void> {
|
||||
if (await page.locator('.cdk-overlay-backdrop').isVisible().catch(() => false)) {
|
||||
await page.keyboard.press('Escape');
|
||||
}
|
||||
}
|
||||
|
||||
test('@stalker series watched toggle — embedded series marks and clears from the header menu', async ({
|
||||
page,
|
||||
request,
|
||||
@@ -1173,14 +1189,10 @@ test('@stalker series watched toggle — embedded series marks and clears from t
|
||||
})
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// The ⋮ trigger sits after the view toggle (data-test-id with a dash —
|
||||
// getByTestId only matches data-testid in this suite; the menu item
|
||||
// renders into the CDK overlay).
|
||||
const menuTrigger = page.locator('[data-test-id="series-watch-menu"]');
|
||||
await expect(menuTrigger).toBeVisible();
|
||||
await menuTrigger.click();
|
||||
const seriesToggle = page.locator('[data-test-id="toggle-series-watched"]');
|
||||
await expect(seriesToggle).toBeVisible();
|
||||
// The series row sits in the hero's "…" menu (data-test-id with a dash —
|
||||
// getByTestId only matches data-testid in this suite; the row renders
|
||||
// into the CDK overlay).
|
||||
let seriesToggle = await seriesMenuRow(page, 'toggle-series-watched');
|
||||
await expect(seriesToggle).toContainText(
|
||||
`Mark series as watched (${episodeCount})`
|
||||
);
|
||||
@@ -1192,12 +1204,12 @@ test('@stalker series watched toggle — embedded series marks and clears from t
|
||||
await expect(watchedCards).toHaveCount(episodeCount, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await menuTrigger.click();
|
||||
seriesToggle = await seriesMenuRow(page, 'toggle-series-watched');
|
||||
await expect(seriesToggle).toContainText('Mark series as unwatched');
|
||||
await seriesToggle.click();
|
||||
|
||||
await expect(watchedCards).toHaveCount(0, { timeout: 15_000 });
|
||||
await menuTrigger.click();
|
||||
seriesToggle = await seriesMenuRow(page, 'toggle-series-watched');
|
||||
await expect(seriesToggle).toContainText(
|
||||
`Mark series as watched (${episodeCount})`
|
||||
);
|
||||
@@ -1205,7 +1217,7 @@ test('@stalker series watched toggle — embedded series marks and clears from t
|
||||
const detailUrl = page.url();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(seriesToggle).toBeHidden();
|
||||
await menuTrigger.focus();
|
||||
await page.locator('[data-testid="series-more-menu"]').focus();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.locator('app-portal-detail-shell')).toHaveCount(0);
|
||||
await expect(page).toHaveURL(detailUrl);
|
||||
|
||||
@@ -656,24 +656,26 @@ test('@xtream season watched toggle — marks a season, survives reload, and cle
|
||||
|
||||
// Serial details: default scenario has 3 seasons × 8 episodes and no
|
||||
// playback positions yet, so season 1 is auto-selected fully unwatched.
|
||||
const seasonToggle = page.locator('[data-test-id="toggle-season-watched"]');
|
||||
await expect(seasonToggle).toBeVisible({ timeout: 15_000 });
|
||||
// The season toggle is a row of the hero's "…" menu.
|
||||
let seasonToggle = await seriesMenuRow(page, 'toggle-season-watched');
|
||||
await expect(seasonToggle).toContainText('Mark season as watched (8)');
|
||||
await closeSeriesMenu(page);
|
||||
|
||||
const episodeCards = page.locator('.episode-card');
|
||||
await expect(episodeCards).toHaveCount(8, { timeout: 10_000 });
|
||||
const watchedCards = page.locator('.episode-card--watched');
|
||||
await expect(watchedCards).toHaveCount(0);
|
||||
|
||||
seasonToggle = await seriesMenuRow(page, 'toggle-season-watched');
|
||||
await seasonToggle.click();
|
||||
|
||||
// Full-progress rows land for all 8 episodes: the button flips, every
|
||||
// Full-progress rows land for all 8 episodes: the row flips, every
|
||||
// episode card gets the watched state, and the per-episode toggles show
|
||||
// the filled check.
|
||||
await expect(seasonToggle).toContainText('Mark season as unwatched', {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(watchedCards).toHaveCount(8);
|
||||
await expect(watchedCards).toHaveCount(8, { timeout: 15_000 });
|
||||
seasonToggle = await seriesMenuRow(page, 'toggle-season-watched');
|
||||
await expect(seasonToggle).toContainText('Mark season as unwatched');
|
||||
await closeSeriesMenu(page);
|
||||
await expect(
|
||||
page.locator(
|
||||
'[data-testid="episode-watched-toggle"].episode-card__watched-toggle--watched'
|
||||
@@ -696,8 +698,9 @@ test('@xtream season watched toggle — marks a season, survives reload, and cle
|
||||
// fresh mount auto-selects the earliest season with unwatched episodes
|
||||
// (issue #1441), so season 2 opens while season 1's tab keeps the check.
|
||||
await page.reload();
|
||||
await expect(seasonToggle).toBeVisible({ timeout: 20_000 });
|
||||
seasonToggle = await seriesMenuRow(page, 'toggle-season-watched');
|
||||
await expect(seasonToggle).toContainText('Mark season as watched (8)');
|
||||
await closeSeriesMenu(page);
|
||||
await expect(watchedCards).toHaveCount(0, { timeout: 10_000 });
|
||||
await expect(
|
||||
seasonTabs.first().locator('.season-tabs__done')
|
||||
@@ -795,6 +798,23 @@ test('@xtream season cover — shows the provider season cover and follows the s
|
||||
// unwatch-all once the whole series is watched, and survives a reload.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
/** Opens the series "…" menu (if closed) and returns the row with that test id. */
|
||||
async function seriesMenuRow(page: Page, rowTestId: string) {
|
||||
const row = page.locator(`[data-test-id="${rowTestId}"]`);
|
||||
if (!(await row.isVisible().catch(() => false))) {
|
||||
await page.locator('[data-testid="series-more-menu"]').click();
|
||||
}
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
return row;
|
||||
}
|
||||
|
||||
async function closeSeriesMenu(page: Page): Promise<void> {
|
||||
if (await page.locator('.cdk-overlay-backdrop').isVisible().catch(() => false)) {
|
||||
await page.keyboard.press('Escape');
|
||||
}
|
||||
}
|
||||
|
||||
test('@xtream series watched toggle — marks every season from the header menu, survives reload, and clears again', async ({
|
||||
page,
|
||||
request,
|
||||
@@ -830,17 +850,13 @@ test('@xtream series watched toggle — marks every season from the header menu,
|
||||
await expect(seriesCard).toBeVisible({ timeout: 10_000 });
|
||||
await seriesCard.click();
|
||||
|
||||
// The ⋮ series menu sits at the end of the season-header actions row
|
||||
// (data-test-id with a dash — getByTestId only matches data-testid in
|
||||
// this suite; the menu item renders into the CDK overlay).
|
||||
const menuTrigger = page.locator('[data-test-id="series-watch-menu"]');
|
||||
await expect(menuTrigger).toBeVisible({ timeout: 15_000 });
|
||||
// The series row sits in the hero's "…" menu (data-test-id with a dash —
|
||||
// getByTestId only matches data-testid in this suite; the row renders
|
||||
// into the CDK overlay).
|
||||
const seasonTabs = page.locator('.season-tabs__pill');
|
||||
await expect(seasonTabs).toHaveCount(3);
|
||||
|
||||
await menuTrigger.click();
|
||||
const seriesToggle = page.locator('[data-test-id="toggle-series-watched"]');
|
||||
await expect(seriesToggle).toBeVisible();
|
||||
let seriesToggle = await seriesMenuRow(page, 'toggle-series-watched');
|
||||
await expect(seriesToggle).toContainText('Mark series as watched (24)');
|
||||
await seriesToggle.click();
|
||||
|
||||
@@ -852,27 +868,29 @@ test('@xtream series watched toggle — marks every season from the header menu,
|
||||
await expect(page.locator('.episode-card--watched')).toHaveCount(8);
|
||||
|
||||
// The action now offers unwatch-all.
|
||||
await menuTrigger.click();
|
||||
seriesToggle = await seriesMenuRow(page, 'toggle-series-watched');
|
||||
await expect(seriesToggle).toContainText('Mark series as unwatched');
|
||||
await page.keyboard.press('Escape');
|
||||
await closeSeriesMenu(page);
|
||||
|
||||
// PWA persistence: positions live in localStorage, so a reload must come
|
||||
// back fully watched across all seasons.
|
||||
await page.reload();
|
||||
await expect(menuTrigger).toBeVisible({ timeout: 20_000 });
|
||||
await expect(page.locator('[data-testid="series-more-menu"]')).toBeVisible(
|
||||
{ timeout: 20_000 }
|
||||
);
|
||||
await expect(page.locator('.season-tabs__done')).toHaveCount(3, {
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// Unwatch-all clears every season again.
|
||||
await menuTrigger.click();
|
||||
seriesToggle = await seriesMenuRow(page, 'toggle-series-watched');
|
||||
await expect(seriesToggle).toContainText('Mark series as unwatched');
|
||||
await seriesToggle.click();
|
||||
await expect(page.locator('.season-tabs__done')).toHaveCount(0, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(page.locator('.episode-card--watched')).toHaveCount(0);
|
||||
await menuTrigger.click();
|
||||
seriesToggle = await seriesMenuRow(page, 'toggle-series-watched');
|
||||
await expect(seriesToggle).toContainText('Mark series as watched (24)');
|
||||
});
|
||||
|
||||
@@ -989,7 +1007,9 @@ for (const theme of ['light', 'dark']) {
|
||||
(dark) => document.body.classList.toggle('dark-theme', dark),
|
||||
theme === 'dark'
|
||||
);
|
||||
const favorite = shell.locator('.favorite-btn').first();
|
||||
const favorite = shell
|
||||
.locator('[data-testid="series-favorite-toggle"]')
|
||||
.first();
|
||||
const card = shell.locator('.episode-card').first();
|
||||
const toggle = shell.locator('mat-button-toggle-group');
|
||||
await expect(card).toBeVisible();
|
||||
|
||||
Reference in new issue
Block a user