From a8253c580179a5ee6fa89f08e04f112e8849a376 Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 19 May 2026 22:07:26 +0200 Subject: [PATCH] test(e2e): adapt to v0.22 dialog + theme picker redesigns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The redesign rollup in this branch replaced the Add Playlist dialog's 2-level type × subtype tabs with a single flat 5-card radiogroup, and replaced the Settings theme list with a compact segmented control whose options are just "Light"/"Dark"/"System" (no "theme" suffix). E2E tests that pinned to the old roles/labels failed in CI: - basic / xtream / stalker / self-hosted: getByRole('tab', ...) on "Add via file upload" / "Xtream" / "Stalker" — there are no tabs in the new dialog. Switched to getByRole('radio', { name: /M3U file/i }) and friends, matched against the new radio labels. - settings: getByRole('radio', { name: 'System theme' }) — the new picker uses just "System". Scoped to the [data-test-id="select-theme"] radiogroup so it doesn't collide with the identically-labelled cover-size options below. The Electron `clickDialogSegmentedOption` helper grew a radio-role lookup as its primary path and keeps the old tab/button/legacy-selector fallbacks so a future redesign won't break every fixture again. The "M3U" parent category becomes a no-op (the new picker has no standalone M3U tile — callers always specialise to M3U URL/file/text immediately after, which is what we want anyway). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/electron-test-fixtures.ts | 104 ++++++++++-------- apps/web-e2e/src/basic.e2e.ts | 3 +- apps/web-e2e/src/self-hosted.e2e.ts | 8 +- apps/web-e2e/src/settings.e2e.ts | 15 ++- apps/web-e2e/src/stalker.e2e.ts | 3 +- apps/web-e2e/src/xtream.e2e.ts | 5 +- 6 files changed, 86 insertions(+), 52 deletions(-) diff --git a/apps/electron-backend-e2e/src/electron-test-fixtures.ts b/apps/electron-backend-e2e/src/electron-test-fixtures.ts index 6757825e4..9589992ad 100644 --- a/apps/electron-backend-e2e/src/electron-test-fixtures.ts +++ b/apps/electron-backend-e2e/src/electron-test-fixtures.ts @@ -273,7 +273,7 @@ export async function importM3uPlaylistFromNativeDialog( await clickDialogCategoryOption(dialog, /^m3u$/i); await clickDialogSubtypeOption( dialog, - /add\s+via\s+file\s+upload/i, + /m3u\s*file|add\s+via\s+file\s+upload/i, 'mat-button-toggle[value="file"]' ); const fileInput = dialog.locator('input[type="file"][name="playlist"]'); @@ -364,7 +364,7 @@ export async function addXtreamPortal( const dialog = await getActiveDialog(page); await clickDialogCategoryOption( dialog, - /^xtream$/i, + /xtream(\s+credentials)?/i, 'mat-button-toggle[value="xtream"]' ); @@ -409,57 +409,46 @@ async function setInputValue(input: Locator, value: string): Promise { await expect(input).toHaveValue(value); } -async function clickDialogCategoryOption( +/** + * Pick a source method on the Add Playlist dialog. Since v0.22 the dialog + * exposes a single flat 5-card radiogroup ("M3U URL", "M3U file", "Xtream + * credentials", "Stalker portal", "Raw m3u text") instead of the legacy + * 2-level category × subtype tabs/toggles. The helper still falls through + * to the old tab/button/legacy-selector lookups so we don't have to rewrite + * every call-site on each redesign — but the radio-based picker is the + * primary path now. + */ +async function clickDialogMethodOption( dialog: Locator, label: RegExp, legacySelector?: string ): Promise { - await clickDialogSegmentedOption( - dialog, - 'Playlist category', - label, - legacySelector - ); -} - -async function clickDialogSubtypeOption( - dialog: Locator, - label: RegExp, - legacySelector?: string -): Promise { - await clickDialogSegmentedOption( - dialog, - 'M3U source', - label, - legacySelector - ); -} - -async function clickDialogSegmentedOption( - dialog: Locator, - tablistLabel: string, - label: RegExp, - legacySelector?: string -): Promise { - const tablist = dialog - .locator(`[role="tablist"][aria-label="${tablistLabel}"]`) + const optionByRadio = dialog + .getByRole('radio', { name: label }) .first(); + if ((await optionByRadio.count()) > 0) { + await optionByRadio.click(); + return; + } - if ((await tablist.count()) > 0) { - const optionByTabRole = tablist - .getByRole('tab', { name: label }) + for (const tablistLabel of ['Source method', 'Playlist category', 'M3U source']) { + const tablist = dialog + .locator(`[role="tablist"][aria-label="${tablistLabel}"]`) .first(); - - if ((await optionByTabRole.count()) > 0) { - await optionByTabRole.click(); - return; + if ((await tablist.count()) > 0) { + const optionByTabRole = tablist + .getByRole('tab', { name: label }) + .first(); + if ((await optionByTabRole.count()) > 0) { + await optionByTabRole.click(); + return; + } } } const optionByGlobalTabRole = dialog .getByRole('tab', { name: label }) .first(); - if ((await optionByGlobalTabRole.count()) > 0) { await optionByGlobalTabRole.click(); return; @@ -468,7 +457,6 @@ async function clickDialogSegmentedOption( const optionByButtonRole = dialog .getByRole('button', { name: label }) .first(); - if ((await optionByButtonRole.count()) > 0) { await optionByButtonRole.click(); return; @@ -476,13 +464,41 @@ async function clickDialogSegmentedOption( if (!legacySelector) { throw new Error( - `Could not find dialog option matching ${label} in "${tablistLabel}".` + `Could not find dialog option matching ${label}.` ); } await dialog.locator(legacySelector).click(); } +// Backwards-compat shims for the legacy two-step flow. Both helpers now route +// through `clickDialogMethodOption` and use the patterns of the new flat +// picker. `clickDialogCategoryOption` is a no-op for "M3U" since the new +// picker has no parent "M3U" tile — callers immediately follow up with a +// `clickDialogSubtypeOption` which picks the concrete M3U URL/file/text card. +async function clickDialogCategoryOption( + dialog: Locator, + label: RegExp, + legacySelector?: string +): Promise { + // The legacy "M3U" category is now implicit — the new picker has no + // standalone "M3U" radio; callers always immediately specialise via + // `clickDialogSubtypeOption` below. Skip the click to avoid matching + // unrelated radios (e.g. "M3U URL" when caller wanted "M3U file"). + if (/^\^?m3u\$?$/i.test(label.source)) { + return; + } + await clickDialogMethodOption(dialog, label, legacySelector); +} + +async function clickDialogSubtypeOption( + dialog: Locator, + label: RegExp, + legacySelector?: string +): Promise { + await clickDialogMethodOption(dialog, label, legacySelector); +} + export async function addStalkerPortal( page: Page, options: { @@ -501,7 +517,7 @@ export async function addStalkerPortal( const dialog = await getActiveDialog(page); await clickDialogCategoryOption( dialog, - /^stalker$/i, + /stalker(\s+portal)?/i, 'mat-button-toggle[value="stalker"]' ); @@ -576,7 +592,7 @@ export async function importM3uPlaylistFromUrl( await clickDialogCategoryOption(dialog, /^m3u$/i); await clickDialogSubtypeOption( dialog, - /add\s+via\s+url/i, + /m3u\s*url|add\s+via\s+url/i, 'mat-button-toggle[value="url"]' ); diff --git a/apps/web-e2e/src/basic.e2e.ts b/apps/web-e2e/src/basic.e2e.ts index 6a6262ac8..6e8040ed5 100644 --- a/apps/web-e2e/src/basic.e2e.ts +++ b/apps/web-e2e/src/basic.e2e.ts @@ -13,7 +13,8 @@ test('basic test', async ({ page }) => { await page.getByRole('button', { name: 'Add playlist' }).click(); const dialog = page.locator('mat-dialog-container'); await expect(dialog).toBeVisible(); - await dialog.getByRole('tab', { name: 'Add via file upload' }).click(); + // v0.22 redesign: tabs were replaced with a flat 5-card radio picker. + await dialog.getByRole('radio', { name: /M3U file/i }).click(); await page.setInputFiles('input[type="file"]', fixturePath); const addButton = dialog.getByRole('button', { name: 'Add playlist', diff --git a/apps/web-e2e/src/self-hosted.e2e.ts b/apps/web-e2e/src/self-hosted.e2e.ts index 0529f4baa..332bb6479 100644 --- a/apps/web-e2e/src/self-hosted.e2e.ts +++ b/apps/web-e2e/src/self-hosted.e2e.ts @@ -36,7 +36,10 @@ async function addXtreamPortal(page: Page): Promise { await page.getByRole('button', { name: 'Add playlist' }).click(); const dialog = page.locator('mat-dialog-container'); await expect(dialog).toBeVisible(); - await dialog.getByRole('tab', { name: 'Xtream', exact: true }).click(); + // v0.22 redesign: tabs were replaced with a flat 5-card radio picker. + await dialog + .getByRole('radio', { name: /Xtream credentials/i }) + .click(); await dialog.locator('#title').fill('Self-hosted Xtream'); await dialog.locator('#serverUrl').fill(XTREAM_MOCK_SERVER); @@ -52,7 +55,8 @@ async function addStalkerPortal(page: Page): Promise { await page.getByRole('button', { name: 'Add playlist' }).click(); const dialog = page.locator('mat-dialog-container'); await expect(dialog).toBeVisible(); - await dialog.getByRole('tab', { name: 'Stalker', exact: true }).click(); + // v0.22 redesign: tabs were replaced with a flat 5-card radio picker. + await dialog.getByRole('radio', { name: /Stalker portal/i }).click(); await setInputValue(dialog.locator('input#title'), 'Self-hosted Stalker'); await setInputValue(dialog.locator('input#portalUrl'), STALKER_PORTAL_URL); diff --git a/apps/web-e2e/src/settings.e2e.ts b/apps/web-e2e/src/settings.e2e.ts index 3b18bc6e1..f7ba49b2b 100644 --- a/apps/web-e2e/src/settings.e2e.ts +++ b/apps/web-e2e/src/settings.e2e.ts @@ -49,17 +49,26 @@ test.describe('Settings', () => { test('Change app theme', async ({ page }) => { await openSettings(page); + // v0.22 compact theme picker exposes the segmented control as a + // radiogroup with options labelled just "Light"/"Dark"/"System". + // Scope to the theme radiogroup so we don't collide with the + // identically-labelled cover-size options below. + const themeGroup = page.locator( + '[data-test-id="select-theme"][role="radiogroup"]' + ); await expect( - page.getByRole('radio', { name: 'System theme' }) + themeGroup.getByRole('radio', { name: 'System', exact: true }) ).toHaveAttribute('aria-checked', 'true'); - await page.getByRole('radio', { name: 'Dark theme' }).click(); + await themeGroup + .getByRole('radio', { name: 'Dark', exact: true }) + .click(); await saveSettings(page); await page.reload(); await openSettings(page); await expect( - page.getByRole('radio', { name: 'Dark theme' }) + themeGroup.getByRole('radio', { name: 'Dark', exact: true }) ).toHaveAttribute('aria-checked', 'true'); }); diff --git a/apps/web-e2e/src/stalker.e2e.ts b/apps/web-e2e/src/stalker.e2e.ts index aa8e8bc5e..11695285a 100644 --- a/apps/web-e2e/src/stalker.e2e.ts +++ b/apps/web-e2e/src/stalker.e2e.ts @@ -126,7 +126,8 @@ async function addStalkerPortal( await page.getByRole('button', { name: 'Add playlist' }).click(); const dialog = page.locator('mat-dialog-container'); await expect(dialog).toBeVisible(); - await dialog.getByRole('tab', { name: 'Stalker', exact: true }).click(); + // v0.22 redesign: tabs were replaced with a flat 5-card radio picker. + await dialog.getByRole('radio', { name: /Stalker portal/i }).click(); await setInputValue(dialog.locator('input#title'), name); await setInputValue(dialog.locator('input#portalUrl'), PORTAL_URL); diff --git a/apps/web-e2e/src/xtream.e2e.ts b/apps/web-e2e/src/xtream.e2e.ts index f1c54b529..2cf44e57b 100644 --- a/apps/web-e2e/src/xtream.e2e.ts +++ b/apps/web-e2e/src/xtream.e2e.ts @@ -78,7 +78,10 @@ async function addXtreamPortal( await page.getByRole('button', { name: 'Add playlist' }).click(); const dialog = page.locator('mat-dialog-container'); await expect(dialog).toBeVisible(); - await dialog.getByRole('tab', { name: 'Xtream', exact: true }).click(); + // v0.22 redesign: tabs were replaced with a flat 5-card radio picker. + await dialog + .getByRole('radio', { name: /Xtream credentials/i }) + .click(); await dialog.locator('#title').fill(name); await dialog.locator('#serverUrl').fill(MOCK_SERVER);