mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
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>
860 lines
33 KiB
TypeScript
860 lines
33 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';
|
|
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 ({
|
|
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);
|
|
await routePlayableStreams(app.mainWindow);
|
|
|
|
try {
|
|
await importM3uPlaylistFromNativeDialog(app, filePath);
|
|
await waitForM3uCatalog(app.mainWindow);
|
|
|
|
await startAndConfirmPlayback(app.mainWindow, () =>
|
|
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, persists favorites after restart, and clears only after one confirmation', 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);
|
|
await routePlayableStreams(app.mainWindow);
|
|
|
|
try {
|
|
await importM3uPlaylistFromNativeDialog(app, filePath);
|
|
await waitForM3uCatalog(app.mainWindow);
|
|
|
|
await startAndConfirmPlayback(app.mainWindow, () =>
|
|
channelItemByTitle(app.mainWindow, 'Recent Channel One')
|
|
.first()
|
|
.click()
|
|
);
|
|
await startAndConfirmPlayback(app.mainWindow, () =>
|
|
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);
|
|
const recentPath = sourceRecentPath(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 });
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
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);
|
|
await routePlayableStreams(app.mainWindow);
|
|
|
|
try {
|
|
await importM3uPlaylistFromNativeDialog(app, filePath);
|
|
await waitForM3uCatalog(app.mainWindow);
|
|
|
|
await startAndConfirmPlayback(app.mainWindow, () =>
|
|
channelItemByTitle(app.mainWindow, 'Context Recent One')
|
|
.first()
|
|
.click()
|
|
);
|
|
await startAndConfirmPlayback(app.mainWindow, () =>
|
|
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);
|
|
await routePlayableStreams(app.mainWindow);
|
|
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
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);
|
|
const recentPath = sourceRecentPath(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 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);
|
|
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 and preserves history through the movie portal handoff', 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);
|
|
await routePlayableStreams(app.mainWindow);
|
|
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
playCurrentDetail(app.mainWindow)
|
|
);
|
|
const moviePath = new URL(app.mainWindow.url()).pathname;
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
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,
|
|
});
|
|
|
|
const viewInPortal = app.mainWindow.getByRole('button', {
|
|
name: 'View in portal',
|
|
exact: true,
|
|
});
|
|
await viewInPortal.click();
|
|
await expect
|
|
.poll(() => new URL(app.mainWindow.url()).pathname)
|
|
.toBe(moviePath);
|
|
await expect(
|
|
app.mainWindow.locator('app-content-hero')
|
|
).toContainText(movieTitle);
|
|
await expect(viewInPortal).toHaveCount(0);
|
|
await startAndConfirmPlayback(app.mainWindow, () =>
|
|
playCurrentDetail(app.mainWindow)
|
|
);
|
|
|
|
await goBackFromDetail(app.mainWindow);
|
|
await expectInlineCollectionDetail(app.mainWindow, {
|
|
pathname: /\/workspace\/global-recent$/,
|
|
title: movieTitle,
|
|
playlistTitle: portalTitle,
|
|
});
|
|
await app.mainWindow.goForward();
|
|
await expect
|
|
.poll(() => new URL(app.mainWindow.url()).pathname)
|
|
.toBe(moviePath);
|
|
await expect(
|
|
app.mainWindow.locator('app-content-hero')
|
|
).toContainText(movieTitle);
|
|
await app.mainWindow.goBack();
|
|
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, preserves it after restart, and clears only after one confirmation', 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);
|
|
await routePlayableStreams(app.mainWindow);
|
|
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
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);
|
|
const recentPath = sourceRecentPath(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);
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
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);
|
|
await routePlayableStreams(app.mainWindow);
|
|
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
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 startAndConfirmPlayback(app.mainWindow, () =>
|
|
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 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 header's Back is route-level in browse and
|
|
// watch alike (closing the player is the bar's own Close button).
|
|
const backButton = page.getByTestId('workspace-header-back');
|
|
|
|
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.getByTestId('workspace-header-back')).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()));
|
|
}
|