mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
test(e2e): adapt to v0.22 dialog + theme picker redesigns
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) <noreply@anthropic.com>
This commit is contained in:
1 parent
8091dec074
commit
a8253c5801
6 files changed
+86
-52
No files matched your search
@@ -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<void> {
|
||||
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<void> {
|
||||
await clickDialogSegmentedOption(
|
||||
dialog,
|
||||
'Playlist category',
|
||||
label,
|
||||
legacySelector
|
||||
);
|
||||
}
|
||||
|
||||
async function clickDialogSubtypeOption(
|
||||
dialog: Locator,
|
||||
label: RegExp,
|
||||
legacySelector?: string
|
||||
): Promise<void> {
|
||||
await clickDialogSegmentedOption(
|
||||
dialog,
|
||||
'M3U source',
|
||||
label,
|
||||
legacySelector
|
||||
);
|
||||
}
|
||||
|
||||
async function clickDialogSegmentedOption(
|
||||
dialog: Locator,
|
||||
tablistLabel: string,
|
||||
label: RegExp,
|
||||
legacySelector?: string
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
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"]'
|
||||
);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -36,7 +36,10 @@ async function addXtreamPortal(page: Page): Promise<void> {
|
||||
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<void> {
|
||||
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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user