mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 06:06:18 -08:00
chore: resolve master conflicts for embedded mpv branch
Entire-Checkpoint: 5b514fe72836
This commit is contained in:
commit
ec2f6a702a
290 files changed
+12183
-4142
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import {
|
||||
addXtreamPortal,
|
||||
channelItemByTitle,
|
||||
clickCategoryByNameExact,
|
||||
clickFirstGridListCard,
|
||||
closeElectronApp,
|
||||
@@ -94,8 +95,15 @@ test.describe('Dashboard Activation', () => {
|
||||
/\/workspace\/xtreams\/[^/]+\/favorites$/
|
||||
);
|
||||
await expect(
|
||||
app.mainWindow.locator('.player-toolbar .close-btn').first()
|
||||
app.mainWindow
|
||||
.locator(
|
||||
'app-unified-live-tab .content-container .video-player'
|
||||
)
|
||||
.first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, liveTitle).first()
|
||||
).toHaveClass(/(^|\s)active(\s|$)/);
|
||||
|
||||
await app.mainWindow.goBack();
|
||||
await app.mainWindow.waitForURL(/\/workspace\/dashboard$/);
|
||||
|
||||
@@ -73,21 +73,23 @@ test.describe('Electron Recently Viewed', () => {
|
||||
app.mainWindow,
|
||||
'Stable Recent Channel'
|
||||
).first();
|
||||
const closeButton = app.mainWindow
|
||||
.locator('.player-toolbar .close-btn')
|
||||
const livePlayer = app.mainWindow
|
||||
.locator('app-unified-live-tab .content-container .video-player')
|
||||
.first();
|
||||
|
||||
await item.click();
|
||||
await expect(closeButton).toBeVisible({ timeout: 20000 });
|
||||
await expect(livePlayer).toBeVisible({ timeout: 20000 });
|
||||
await expect(item).toHaveClass(/(^|\s)active(\s|$)/);
|
||||
|
||||
await item.click();
|
||||
await expect(closeButton).toBeVisible({ timeout: 20000 });
|
||||
await expect(livePlayer).toBeVisible({ timeout: 20000 });
|
||||
await expect(item).toHaveClass(/(^|\s)active(\s|$)/);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('tracks M3U recent channels in newest-first order, supports all-playlists scope, and persists removals after restart', async ({
|
||||
test('tracks M3U recent channels in newest-first order, supports all-playlists scope, and persists favorites after restart', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const playlistTitle = 'm3u-recent-source.m3u';
|
||||
@@ -127,10 +129,15 @@ test.describe('Electron Recently Viewed', () => {
|
||||
.poll(() => visibleLiveTitles(app.mainWindow))
|
||||
.toEqual(['Recent Channel Two', 'Recent Channel One']);
|
||||
|
||||
await removeLiveRecentItem(app.mainWindow, 'Recent Channel Two');
|
||||
await toggleFavoriteForChannel(app.mainWindow, 'Recent Channel Two');
|
||||
await expect
|
||||
.poll(() => visibleLiveTitles(app.mainWindow))
|
||||
.toEqual(['Recent Channel One']);
|
||||
.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;
|
||||
@@ -145,7 +152,12 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await switchUnifiedCollectionScope(app.mainWindow, 'All playlists');
|
||||
await expect
|
||||
.poll(() => visibleLiveTitles(app.mainWindow))
|
||||
.toEqual(['Recent Channel One']);
|
||||
.toEqual(['Recent Channel Two', 'Recent Channel One']);
|
||||
|
||||
await openPlaylistFavorites(app.mainWindow);
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, 'Recent Channel Two').first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
@@ -189,7 +201,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await toggleFavoriteForChannel(app.mainWindow, liveTitle);
|
||||
await openPlaylistFavorites(app.mainWindow);
|
||||
await channelItemByTitle(app.mainWindow, liveTitle).first().click();
|
||||
await closeUnifiedLiveDetail(app.mainWindow);
|
||||
await expectUnifiedLiveDetailOpen(app.mainWindow, liveTitle);
|
||||
|
||||
await app.mainWindow
|
||||
.getByRole('link', { name: 'Movies', exact: true })
|
||||
@@ -396,7 +408,7 @@ test.describe('Electron Recently Viewed', () => {
|
||||
await toggleFavoriteForChannel(app.mainWindow, liveTitle);
|
||||
await openPlaylistFavorites(app.mainWindow);
|
||||
await channelItemByTitle(app.mainWindow, liveTitle).first().click();
|
||||
await closeUnifiedLiveDetail(app.mainWindow);
|
||||
await expectUnifiedLiveDetailOpen(app.mainWindow, liveTitle);
|
||||
|
||||
await app.mainWindow
|
||||
.getByRole('link', { name: 'Movies', exact: true })
|
||||
@@ -561,11 +573,18 @@ async function clearRecentItems(page: Page, typeLabel: string): Promise<void> {
|
||||
await page.getByRole('button', { name: 'Yes' }).click();
|
||||
}
|
||||
|
||||
async function closeUnifiedLiveDetail(page: Page): Promise<void> {
|
||||
const closeButton = page.locator('.player-toolbar .close-btn').first();
|
||||
|
||||
await expect(closeButton).toBeVisible({ timeout: 20000 });
|
||||
await closeButton.click();
|
||||
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> {
|
||||
@@ -634,14 +653,6 @@ async function playFirstSeriesEpisode(page: Page): Promise<void> {
|
||||
await episodeCard.click();
|
||||
}
|
||||
|
||||
async function removeLiveRecentItem(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();
|
||||
}
|
||||
|
||||
async function toggleFavoriteForChannel(
|
||||
page: Page,
|
||||
title: string
|
||||
@@ -651,6 +662,9 @@ async function toggleFavoriteForChannel(
|
||||
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[]> {
|
||||
|
||||
@@ -31,7 +31,9 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
|
||||
const fixture = await fetchXtreamEpgFixture(request, epgCredentials);
|
||||
const currentProgram = fixture.shortEpg[0];
|
||||
if (!currentProgram) {
|
||||
throw new Error('Expected the Xtream EPG fixture to include a current program.');
|
||||
throw new Error(
|
||||
'Expected the Xtream EPG fixture to include a current program.'
|
||||
);
|
||||
}
|
||||
const app = await launchElectronApp(dataDir, {
|
||||
env: { TZ: timeZone },
|
||||
@@ -45,7 +47,10 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
await openWorkspaceSection(app.mainWindow, 'Live TV');
|
||||
await clickCategoryByNameExact(app.mainWindow, fixture.categoryName);
|
||||
await clickCategoryByNameExact(
|
||||
app.mainWindow,
|
||||
fixture.categoryName
|
||||
);
|
||||
|
||||
const channelRow = channelItemByTitle(
|
||||
app.mainWindow,
|
||||
@@ -55,14 +60,18 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
|
||||
|
||||
await expect
|
||||
.poll(async () =>
|
||||
((await channelRow.locator('.epg-title').textContent()) ?? '').trim()
|
||||
(
|
||||
(await channelRow
|
||||
.locator('.epg-title')
|
||||
.textContent()) ?? ''
|
||||
).trim()
|
||||
)
|
||||
.toBe(currentProgram.title);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return (await channelRow.locator('.epg-time').allInnerTexts()).map(
|
||||
(value) => value.trim()
|
||||
);
|
||||
return (
|
||||
await channelRow.locator('.epg-time').allInnerTexts()
|
||||
).map((value) => value.trim());
|
||||
})
|
||||
.toEqual([
|
||||
formatTimeInZone(currentProgram.startTimestamp, timeZone),
|
||||
@@ -84,22 +93,14 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
|
||||
timeZone
|
||||
);
|
||||
|
||||
await expect.poll(() => visibleProgramTitles(app.mainWindow)).toEqual(
|
||||
currentDayTitles
|
||||
);
|
||||
await expect
|
||||
.poll(() => visibleProgramTitles(app.mainWindow))
|
||||
.toEqual(currentDayTitles);
|
||||
|
||||
const currentProgramRow = app.mainWindow
|
||||
.locator('app-epg-list .program-item.current-program')
|
||||
.first();
|
||||
await expect(currentProgramRow).toBeVisible();
|
||||
await expect(currentProgramRow.locator('.program-title')).toHaveText(
|
||||
currentProgram.title
|
||||
);
|
||||
await expect(currentProgramRow.locator('.time')).toHaveText(
|
||||
`${formatTimeInZone(
|
||||
currentProgram.startTimestamp,
|
||||
timeZone
|
||||
)} - ${formatTimeInZone(currentProgram.stopTimestamp, timeZone)}`
|
||||
await expectCurrentProgram(
|
||||
app.mainWindow,
|
||||
currentProgram,
|
||||
timeZone
|
||||
);
|
||||
|
||||
const firstFutureDateKey = getFirstFutureDateKey(
|
||||
@@ -114,25 +115,29 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
|
||||
firstFutureDateKey!,
|
||||
timeZone
|
||||
);
|
||||
const nextDayClicks = dayDifference(currentDateKey, firstFutureDateKey!);
|
||||
const nextDayClicks = dayDifference(
|
||||
currentDateKey,
|
||||
firstFutureDateKey!
|
||||
);
|
||||
|
||||
for (let index = 0; index < nextDayClicks; index += 1) {
|
||||
await app.mainWindow.locator('app-epg-list .next-day').click();
|
||||
await app.mainWindow
|
||||
.locator(
|
||||
'app-live-epg-panel .next-day, app-epg-list .next-day'
|
||||
)
|
||||
.click();
|
||||
}
|
||||
|
||||
await expect.poll(() => visibleProgramTitles(app.mainWindow)).toEqual(
|
||||
futureDayTitles
|
||||
);
|
||||
await expect
|
||||
.poll(() => visibleProgramTitles(app.mainWindow))
|
||||
.toEqual(futureDayTitles);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatTimeInZone(
|
||||
timestampSeconds: number,
|
||||
timeZone: string
|
||||
): string {
|
||||
function formatTimeInZone(timestampSeconds: number, timeZone: string): string {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
timeZone,
|
||||
hour: '2-digit',
|
||||
@@ -157,7 +162,9 @@ function formatDateKeyInZone(
|
||||
const day = parts.find((part) => part.type === 'day')?.value;
|
||||
|
||||
if (!year || !month || !day) {
|
||||
throw new Error(`Failed to format a date key for timezone ${timeZone}.`);
|
||||
throw new Error(
|
||||
`Failed to format a date key for timezone ${timeZone}.`
|
||||
);
|
||||
}
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
@@ -215,14 +222,48 @@ function dayDifference(fromDateKey: string, toDateKey: string): number {
|
||||
return Math.round((to - from) / (24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
async function visibleProgramTitles(page: Parameters<typeof channelItemByTitle>[0]) {
|
||||
async function visibleProgramTitles(
|
||||
page: Parameters<typeof channelItemByTitle>[0]
|
||||
) {
|
||||
return page
|
||||
.locator('app-epg-list .program-title')
|
||||
.allInnerTexts()
|
||||
.then((titles) => titles.map((title) => title.trim()).filter(Boolean));
|
||||
}
|
||||
|
||||
async function getProgressWidthPercent(row: ReturnType<typeof channelItemByTitle>) {
|
||||
async function expectCurrentProgram(
|
||||
page: Parameters<typeof channelItemByTitle>[0],
|
||||
currentProgram: XtreamNormalizedEpgListing,
|
||||
timeZone: string
|
||||
): Promise<void> {
|
||||
const startTime = formatTimeInZone(currentProgram.startTimestamp, timeZone);
|
||||
const stopTime = formatTimeInZone(currentProgram.stopTimestamp, timeZone);
|
||||
const currentProgramRow = page
|
||||
.locator('app-epg-list .program-item.current-program')
|
||||
.first();
|
||||
|
||||
if (await currentProgramRow.isVisible().catch(() => false)) {
|
||||
await expect(currentProgramRow.locator('.program-title')).toHaveText(
|
||||
currentProgram.title
|
||||
);
|
||||
await expect(currentProgramRow.locator('.time')).toHaveText(
|
||||
`${startTime}–${stopTime}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = page.locator('app-live-epg-panel').first();
|
||||
await expect(summary.locator('.live-epg-panel__title')).toHaveText(
|
||||
currentProgram.title
|
||||
);
|
||||
await expect(summary.locator('.live-epg-panel__time')).toHaveText(
|
||||
`${startTime} - ${stopTime}`
|
||||
);
|
||||
}
|
||||
|
||||
async function getProgressWidthPercent(
|
||||
row: ReturnType<typeof channelItemByTitle>
|
||||
) {
|
||||
const style = await row.locator('.epg-progress-fill').getAttribute('style');
|
||||
const width = style?.match(/width:\s*([\d.]+)%/)?.[1] ?? '0';
|
||||
return Number.parseFloat(width);
|
||||
|
||||
Reference in new issue
Block a user