mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
Chromium drops a keyboard scroll pressed in the frame or two after the previous smooth scroll lands but before Blink handles the compositor's finish: ScrollAnimator retargets the finished curve (kRunningOnCompositorButNeedsUpdate), then the late CompositorAnimationFinished treats that state as finished and resets it. The Live TV responsiveness spec pressed the next key as soon as an expect.poll saw scrollTop change, so a poll that sampled the final frame sent PageDown or Home into that window (2 of 5 local runs on master, all three CI attempts on PR #1886). pressScrollKey presses a scroll key and waits for that scroll's scrollend. The Live TV spec uses it for PageDown, PageDown and Home with the same position assertions; the VOD detail spec swaps its stable-frame fence for it. The validation map documents the race. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
241 lines
8.7 KiB
TypeScript
241 lines
8.7 KiB
TypeScript
import {
|
|
addXtreamPortal,
|
|
clickCategoryByNameExact,
|
|
clickGridListCardByTitle,
|
|
closeElectronApp,
|
|
expect,
|
|
launchElectronApp,
|
|
resetMockServers,
|
|
test,
|
|
waitForXtreamWorkspaceReady,
|
|
xtreamMockServer,
|
|
} from './electron-test-fixtures';
|
|
import { pressScrollKey } from './keyboard-scroll';
|
|
import {
|
|
fetchXtreamVodFixture,
|
|
getXtreamTitle,
|
|
pickDistinctTitles,
|
|
} from './portal-mock-fixtures';
|
|
|
|
const emptyMetadataCredentials = {
|
|
password: 'emptyvod',
|
|
username: 'emptyvod',
|
|
};
|
|
|
|
test.describe('Xtream VOD Details', () => {
|
|
test('keeps a sparse VOD playable inside the curated fallback', async ({
|
|
dataDir,
|
|
request,
|
|
}) => {
|
|
await resetMockServers(request, ['xtream']);
|
|
const vodFixture = await fetchXtreamVodFixture(
|
|
request,
|
|
emptyMetadataCredentials
|
|
);
|
|
const [movieTitle] = pickDistinctTitles(
|
|
vodFixture.items,
|
|
getXtreamTitle
|
|
);
|
|
const movieItem = vodFixture.items.find(
|
|
(item) => getXtreamTitle(item) === movieTitle
|
|
);
|
|
const streamId = Number(movieItem?.stream_id);
|
|
const containerExtension = movieItem?.container_extension?.trim();
|
|
if (
|
|
!Number.isInteger(streamId) ||
|
|
streamId <= 0 ||
|
|
!containerExtension
|
|
) {
|
|
throw new Error(
|
|
'Xtream VOD fixture returned an invalid playback source.'
|
|
);
|
|
}
|
|
const expectedMovieUrl =
|
|
`${xtreamMockServer}/movie/` +
|
|
`${emptyMetadataCredentials.username}/` +
|
|
`${emptyMetadataCredentials.password}/` +
|
|
`${streamId}.${containerExtension}`;
|
|
const app = await launchElectronApp(dataDir);
|
|
|
|
try {
|
|
await addXtreamPortal(app.mainWindow, {
|
|
...emptyMetadataCredentials,
|
|
name: 'Xtream Empty Metadata',
|
|
});
|
|
await waitForXtreamWorkspaceReady(app.mainWindow);
|
|
|
|
await app.mainWindow
|
|
.getByRole('link', { name: 'Movies', exact: true })
|
|
.click();
|
|
await clickCategoryByNameExact(
|
|
app.mainWindow,
|
|
vodFixture.categoryName
|
|
);
|
|
await clickGridListCardByTitle(app.mainWindow, movieTitle);
|
|
|
|
await app.mainWindow.waitForURL(
|
|
/\/workspace\/xtreams\/[^/]+\/vod\/[^/]+\/[^/]+$/
|
|
);
|
|
await expect(
|
|
app.mainWindow.locator('app-content-hero')
|
|
).toContainText(movieTitle);
|
|
await expect(
|
|
app.mainWindow.locator(
|
|
'[data-testid="xtream-vod-fallback-status"]'
|
|
)
|
|
).toContainText('Portal metadata unavailable');
|
|
await expect(
|
|
app.mainWindow.locator('[data-testid="xtream-vod-fallback"]')
|
|
).toContainText(
|
|
'Extended metadata was not provided by this portal.'
|
|
);
|
|
|
|
const playButton = app.mainWindow
|
|
.locator('button.play-btn')
|
|
.first();
|
|
await expect(playButton).toBeVisible();
|
|
// Both secondaries are icon-only on the movie detail now.
|
|
await expect(
|
|
app.mainWindow
|
|
.locator('[data-testid="vod-favorite-toggle"]')
|
|
.first()
|
|
).toBeVisible();
|
|
await expect(
|
|
app.mainWindow
|
|
.locator('[data-testid="vod-download-start"]')
|
|
.first()
|
|
).toBeVisible();
|
|
|
|
const movieResponsePromise = app.mainWindow.waitForResponse(
|
|
(response) =>
|
|
response.url() === expectedMovieUrl &&
|
|
response.request().method() === 'GET',
|
|
{ timeout: 20_000 }
|
|
);
|
|
await playButton.click();
|
|
|
|
const movieResponse = await movieResponsePromise;
|
|
expect(movieResponse.status()).toBe(302);
|
|
await expect(
|
|
app.mainWindow.locator('app-portal-detail-shell')
|
|
).toHaveClass(/shell-host--watch/);
|
|
await expect(
|
|
app.mainWindow
|
|
.locator('app-portal-inline-player app-web-player-view')
|
|
.first()
|
|
).toBeVisible({ timeout: 20_000 });
|
|
await expect(
|
|
app.mainWindow.locator(
|
|
'app-portal-inline-player .player-shell__title'
|
|
)
|
|
).toContainText(movieTitle);
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
});
|
|
|
|
for (const theme of ['light', 'dark']) {
|
|
test(`detail action tooltips preserve one-press Escape (${theme})`, async ({
|
|
dataDir,
|
|
request,
|
|
}) => {
|
|
await resetMockServers(request, ['xtream']);
|
|
const app = await launchElectronApp(dataDir);
|
|
try {
|
|
const page = app.mainWindow;
|
|
await addXtreamPortal(page);
|
|
await waitForXtreamWorkspaceReady(page);
|
|
await page
|
|
.getByRole('link', { name: 'Movies', exact: true })
|
|
.click();
|
|
await page.evaluate(
|
|
(dark) => document.body.classList.toggle('dark-theme', dark),
|
|
theme === 'dark'
|
|
);
|
|
for (const action of [
|
|
'vod-favorite-toggle',
|
|
'vod-download-start',
|
|
]) {
|
|
await page.locator('app-grid-list mat-card').first().click();
|
|
const shell = page.locator('app-portal-detail-shell');
|
|
await expect(
|
|
shell.getByRole('heading', { level: 1 })
|
|
).toBeVisible();
|
|
const button = shell.locator(`[data-testid="${action}"]`);
|
|
await button.focus();
|
|
await button.hover();
|
|
await expect(
|
|
page.locator('.mat-mdc-tooltip-show')
|
|
).toBeVisible();
|
|
await page.keyboard.press('Escape');
|
|
await expect(shell).toHaveCount(0);
|
|
}
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
|
|
test(`supports keyboard and mouse scrolling in portal details (${theme})`, async ({
|
|
dataDir,
|
|
request,
|
|
}) => {
|
|
await resetMockServers(request, ['xtream']);
|
|
const app = await launchElectronApp(dataDir);
|
|
try {
|
|
const page = app.mainWindow;
|
|
await page.setViewportSize({ width: 1200, height: 540 });
|
|
await addXtreamPortal(page);
|
|
await waitForXtreamWorkspaceReady(page);
|
|
for (const section of ['Movies', 'Series']) {
|
|
await page
|
|
.getByRole('link', { name: section, exact: true })
|
|
.click();
|
|
await page.locator('app-grid-list mat-card').first().click();
|
|
const shell = page.locator('app-portal-detail-shell');
|
|
await expect(shell).toBeFocused();
|
|
await page.evaluate(
|
|
(dark) =>
|
|
document.body.classList.toggle('dark-theme', dark),
|
|
theme === 'dark'
|
|
);
|
|
await expect
|
|
.poll(() =>
|
|
shell.evaluate(
|
|
(el) => el.scrollHeight - el.clientHeight
|
|
)
|
|
)
|
|
.toBeGreaterThan(0);
|
|
expect(
|
|
await shell.evaluate(
|
|
(el) => getComputedStyle(el).scrollbarWidth
|
|
)
|
|
).not.toBe('none');
|
|
await pressScrollKey(page, shell, 'PageDown');
|
|
expect(
|
|
await shell.evaluate((el) => el.scrollTop)
|
|
).toBeGreaterThan(0);
|
|
await pressScrollKey(page, shell, 'Home');
|
|
expect(await shell.evaluate((el) => el.scrollTop)).toBe(0);
|
|
const box = (await shell.boundingBox())!;
|
|
await page.mouse.move(
|
|
box.x + box.width / 2,
|
|
box.y + box.height / 2
|
|
);
|
|
await page.mouse.wheel(0, 300);
|
|
await expect
|
|
.poll(() => shell.evaluate((el) => el.scrollTop))
|
|
.toBeGreaterThan(0);
|
|
// Back lives in the header, which never scrolls away.
|
|
const back = page.getByTestId('workspace-header-back');
|
|
await expect(back).toBeInViewport();
|
|
await back.focus();
|
|
await page.keyboard.press('Enter');
|
|
await expect(shell).toHaveCount(0);
|
|
}
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
}
|