mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
fix(collections): one confirmed Clear recently viewed action (#1865)
A playlist's own recently viewed page (/workspace/<provider>/<id>/recent, opened from the dashboard's recently viewed rails) showed two clear buttons: the page's "Clear recently viewed <type>", which confirms through createClearCollectionAction, and a header delete_sweep button that cleared every tab of the playlist at once without asking. Remove the header bulk action end to end, and the unreachable, unconfirmed clear button of the M3U channel list's recent view, so every clear goes through createClearCollectionAction. Drop the two i18n keys only those buttons used. E2E: a shared helper asserts one clear control per page and one confirmation per press; the M3U, Xtream and Stalker recent tests cancel first (rows stay), then clear once on the playlist's own recently viewed page. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
015ea203b7
commit
8fabb88106
39 files changed
+261
-327
No files matched your search
@@ -32,34 +32,13 @@ import {
|
||||
RANGE_SERVER_ETAG,
|
||||
startDownload,
|
||||
} from './downloads.e2e-support';
|
||||
import { navigateWithinWorkspace } from './workspace-route.e2e-support';
|
||||
|
||||
async function openDownloadsPage(page: Page): Promise<void> {
|
||||
await page.getByRole('button', { name: 'Open downloads' }).click();
|
||||
await page.waitForURL(/\/workspace\/downloads(?:\?.*)?$/);
|
||||
}
|
||||
|
||||
async function navigateWithinWorkspace(
|
||||
page: Page,
|
||||
path: string
|
||||
): Promise<void> {
|
||||
const targetPathname = await page.evaluate((target) => {
|
||||
const targetUrl = new URL(window.location.href);
|
||||
const workspaceIndex = targetUrl.pathname.lastIndexOf('/workspace');
|
||||
const rendererPath =
|
||||
workspaceIndex >= 0
|
||||
? targetUrl.pathname.slice(0, workspaceIndex)
|
||||
: targetUrl.pathname.replace(/\/$/, '');
|
||||
|
||||
targetUrl.pathname = `${rendererPath}${target}`;
|
||||
targetUrl.search = '';
|
||||
targetUrl.hash = '';
|
||||
window.history.pushState(null, '', targetUrl);
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
return targetUrl.pathname;
|
||||
}, path);
|
||||
await page.waitForURL((url) => url.pathname === targetPathname);
|
||||
}
|
||||
|
||||
async function getPlaylistId(page: Page, title: string): Promise<string> {
|
||||
const playlists = await page.evaluate(
|
||||
async () => (await window.electron?.dbGetAppPlaylistMetas?.()) ?? []
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
routePlayableStreams,
|
||||
startAndConfirmPlayback,
|
||||
} from './playable-stream-fixture';
|
||||
import { clearRecentItems } from './recent-clear.e2e-support';
|
||||
|
||||
/**
|
||||
* Issue #1458, second report: "all channels disappear after clearing the
|
||||
@@ -160,13 +161,7 @@ test.describe('Live channel rail collapse (#1458)', () => {
|
||||
channelItemByTitle(app.mainWindow, 'Channel Alpha').first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
|
||||
await app.mainWindow
|
||||
.getByRole('button', { name: 'Clear recently viewed Live TV' })
|
||||
.click();
|
||||
await app.mainWindow
|
||||
.locator('mat-dialog-container')
|
||||
.getByRole('button', { name: 'Clear', exact: true })
|
||||
.click();
|
||||
await clearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, 'Channel Alpha')
|
||||
).toHaveCount(0);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
import { expect } from './electron-test-fixtures';
|
||||
import { navigateWithinWorkspace } from './workspace-route.e2e-support';
|
||||
|
||||
/**
|
||||
* `/workspace/<provider>/<id>/recent` of the source the page is in: the
|
||||
* source's own recently viewed page, which the dashboard's recently viewed
|
||||
* rails open.
|
||||
*/
|
||||
export function sourceRecentPath(page: Page): string {
|
||||
const source = /\/workspace\/(?:xtreams|stalker|playlists)\/[^/]+/.exec(
|
||||
new URL(page.url()).pathname
|
||||
);
|
||||
if (!source) {
|
||||
throw new Error(`The page is not inside a source: ${page.url()}`);
|
||||
}
|
||||
return `${source[0]}/recent`;
|
||||
}
|
||||
|
||||
export async function openSourceRecent(
|
||||
page: Page,
|
||||
recentPath: string
|
||||
): Promise<void> {
|
||||
await navigateWithinWorkspace(page, recentPath);
|
||||
}
|
||||
|
||||
/** Every control on the page that clears recently viewed items. */
|
||||
function clearRecentControls(page: Page): Locator {
|
||||
return page.getByRole('button', { name: /^Clear recently viewed\b/ });
|
||||
}
|
||||
|
||||
/**
|
||||
* Presses the page's only "Clear recently viewed <type>" control and returns
|
||||
* the confirmation it opens: one control per page, one question per press.
|
||||
*/
|
||||
async function askToClearRecent(
|
||||
page: Page,
|
||||
typeLabel: string
|
||||
): Promise<Locator> {
|
||||
await expect(clearRecentControls(page)).toHaveCount(1);
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: `Clear recently viewed ${typeLabel}`,
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
const dialogs = page.locator('mat-dialog-container');
|
||||
await expect(dialogs).toHaveCount(1);
|
||||
await expect(
|
||||
dialogs.getByRole('heading', {
|
||||
name: `Clear recently viewed ${typeLabel}?`,
|
||||
})
|
||||
).toBeVisible();
|
||||
return dialogs;
|
||||
}
|
||||
|
||||
export async function cancelClearRecentItems(
|
||||
page: Page,
|
||||
typeLabel: string
|
||||
): Promise<void> {
|
||||
const dialog = await askToClearRecent(page, typeLabel);
|
||||
await dialog.getByRole('button', { name: 'Cancel', exact: true }).click();
|
||||
await expect(page.locator('mat-dialog-container')).toHaveCount(0);
|
||||
}
|
||||
|
||||
export async function clearRecentItems(
|
||||
page: Page,
|
||||
typeLabel: string
|
||||
): Promise<void> {
|
||||
const dialog = await askToClearRecent(page, typeLabel);
|
||||
await dialog.getByRole('button', { name: 'Clear', exact: true }).click();
|
||||
await expect(page.locator('mat-dialog-container')).toHaveCount(0);
|
||||
}
|
||||
@@ -45,6 +45,12 @@ import {
|
||||
routePlayableStreams,
|
||||
startAndConfirmPlayback,
|
||||
} from './playable-stream-fixture';
|
||||
import {
|
||||
cancelClearRecentItems,
|
||||
clearRecentItems,
|
||||
openSourceRecent,
|
||||
sourceRecentPath,
|
||||
} from './recent-clear.e2e-support';
|
||||
|
||||
test.describe('Electron Recently Viewed', () => {
|
||||
test('keeps unified live detail open when re-clicking the active M3U recent item', async ({
|
||||
@@ -98,7 +104,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('@persistence @m3u @electron tracks M3U recent channels in newest-first order, supports all-playlists scope, and persists favorites after restart', async ({
|
||||
test('@persistence @m3u @electron tracks M3U recent channels in newest-first order, supports all-playlists scope, persists favorites after restart, and clears only after one confirmation', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const playlistTitle = 'm3u-recent-source.m3u';
|
||||
@@ -165,6 +171,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
.first()
|
||||
.click();
|
||||
await waitForM3uCatalog(app.mainWindow);
|
||||
const recentPath = sourceRecentPath(app.mainWindow);
|
||||
await openPlaylistRecent(app.mainWindow);
|
||||
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
|
||||
await expect
|
||||
@@ -175,6 +182,19 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, 'Recent Channel Two').first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
|
||||
await openSourceRecent(app.mainWindow, recentPath);
|
||||
await expect
|
||||
.poll(() => visibleLiveTitles(app.mainWindow))
|
||||
.toEqual(['Recent Channel Two', 'Recent Channel One']);
|
||||
await cancelClearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect
|
||||
.poll(() => visibleLiveTitles(app.mainWindow))
|
||||
.toEqual(['Recent Channel Two', 'Recent Channel One']);
|
||||
await clearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect
|
||||
.poll(() => visibleLiveTitles(app.mainWindow))
|
||||
.toEqual([]);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
@@ -340,6 +360,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await openSources(app.mainWindow);
|
||||
await sourceRowByTitle(app.mainWindow, portalTitle).first().click();
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
const recentPath = sourceRecentPath(app.mainWindow);
|
||||
await openPlaylistRecent(app.mainWindow);
|
||||
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
|
||||
@@ -351,7 +372,12 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Series');
|
||||
await expectVisibleContentCardTitle(app.mainWindow, seriesTitle);
|
||||
|
||||
await openSourceRecent(app.mainWindow, recentPath);
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
|
||||
await cancelClearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle)
|
||||
).toHaveCount(1);
|
||||
await clearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle)
|
||||
@@ -496,7 +522,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('@persistence @stalker @electron tracks Stalker live, movie, and series history across playlist and all-playlists scope, and preserves it after restart', async ({
|
||||
test('@persistence @stalker @electron tracks Stalker live, movie, and series history across playlist and all-playlists scope, preserves it after restart, and clears only after one confirmation', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
@@ -593,6 +619,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await openSources(app.mainWindow);
|
||||
await sourceRowByTitle(app.mainWindow, portalTitle).first().click();
|
||||
await waitForStalkerCatalog(app.mainWindow);
|
||||
const recentPath = sourceRecentPath(app.mainWindow);
|
||||
await openPlaylistRecent(app.mainWindow);
|
||||
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
|
||||
@@ -607,6 +634,18 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await expect(
|
||||
contentCardByTitle(app.mainWindow, seriesTitle)
|
||||
).toHaveCount(1);
|
||||
|
||||
await openSourceRecent(app.mainWindow, recentPath);
|
||||
await switchUnifiedCollectionContent(app.mainWindow, 'Live TV');
|
||||
await cancelClearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle)
|
||||
).toHaveCount(1);
|
||||
await clearRecentItems(app.mainWindow, 'Live TV');
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle)
|
||||
).toHaveCount(0);
|
||||
await expectVisibleContentCardTitle(app.mainWindow, movieTitle);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
@@ -709,16 +748,6 @@ const xtreamCredentials = {
|
||||
password: defaultXtreamPassword,
|
||||
};
|
||||
|
||||
async function clearRecentItems(page: Page, typeLabel: string): Promise<void> {
|
||||
await page
|
||||
.getByRole('button', { name: `Clear recently viewed ${typeLabel}` })
|
||||
.click();
|
||||
await page
|
||||
.locator('mat-dialog-container')
|
||||
.getByRole('button', { name: 'Clear', exact: true })
|
||||
.click();
|
||||
}
|
||||
|
||||
async function expectUnifiedLiveDetailOpen(
|
||||
page: Page,
|
||||
title: string
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Moves the renderer to a workspace route without reloading it, as a router
|
||||
* link would. The packaged renderer lives below its own path, so only the
|
||||
* part from `/workspace` on is replaced.
|
||||
*/
|
||||
export async function navigateWithinWorkspace(
|
||||
page: Page,
|
||||
path: string
|
||||
): Promise<void> {
|
||||
const targetPathname = await page.evaluate((target) => {
|
||||
const targetUrl = new URL(window.location.href);
|
||||
const workspaceIndex = targetUrl.pathname.lastIndexOf('/workspace');
|
||||
const rendererPath =
|
||||
workspaceIndex >= 0
|
||||
? targetUrl.pathname.slice(0, workspaceIndex)
|
||||
: targetUrl.pathname.replace(/\/$/, '');
|
||||
|
||||
targetUrl.pathname = `${rendererPath}${target}`;
|
||||
targetUrl.search = '';
|
||||
targetUrl.hash = '';
|
||||
window.history.pushState(null, '', targetUrl);
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
return targetUrl.pathname;
|
||||
}, path);
|
||||
await page.waitForURL((url) => url.pathname === targetPathname);
|
||||
}
|
||||
Reference in new issue
Block a user