mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
* feat(portals): rework the VOD sources popup and action row The Sources popup now anchors above its button and always fits on screen: the overlay caps it to the space beside the chip, only the source list scrolls so the header, search, filter chips and footer stay visible, and it flips below the button when the space above is too small. Filter chips (All / Available / HD+ / language) compose with the playlist search, and "Available" runs check-all itself when nothing has been checked yet. Expanded copy rows no longer repeat the playlist domain: each copy shows its parsed language chip, the provider's raw stream title, and only the tags that differ from the parent copy. Availability checks run at most four at a time and settled verdicts are remembered per movie and source for ten minutes, so reopening a movie no longer re-contacts every foreign portal. Favorites and Download become icon-only buttons with real state: a filled heart when favorited, and a download icon that turns into a progress ring and then a checkmark that reveals the finished file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(portals): match the movie detail's icon-only favorite button The Xtream movie detail's favorite control is now an icon-only button, so the shared "add this detail to favorites" helpers no longer found it by class. They are used against series and Stalker details too, which still render the labeled variant, so they now select by accessible name — the icon button carries the same label in aria-label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): keep provider playback for a downloaded movie Once a movie is downloaded the primary button plays the local file, and the sources popover only exists when another playlist carries the same film. The icon-only rework left those as the only two paths, so a downloaded movie in a single-playlist library had no way at all to stream the provider's copy — the labeled action that used to do it was gone. Restores it as an icon button beside the downloaded checkmark, under the same condition the old one used. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
import {
|
|
addXtreamPortal,
|
|
clickCategoryByNameExact,
|
|
clickGridListCardByTitle,
|
|
closeElectronApp,
|
|
expect,
|
|
launchElectronApp,
|
|
resetMockServers,
|
|
test,
|
|
waitForXtreamWorkspaceReady,
|
|
} from './electron-test-fixtures';
|
|
import {
|
|
fetchXtreamVodFixture,
|
|
getXtreamTitle,
|
|
pickDistinctTitles,
|
|
} from './portal-mock-fixtures';
|
|
|
|
/**
|
|
* `multisrc1` and `multisrc2` are two distinct credential pairs that share one
|
|
* faker seed, so both portals generate an IDENTICAL catalog. That overlap is
|
|
* the whole premise of this feature: the same movie in two playlists.
|
|
*/
|
|
const portalA = { username: 'multisrc1', password: 'multisrc1' };
|
|
const portalB = { username: 'multisrc2', password: 'multisrc2' };
|
|
|
|
test.describe('VOD multi-source', () => {
|
|
test('offers the same movie from another playlist and switches to it', async ({
|
|
dataDir,
|
|
request,
|
|
}) => {
|
|
await resetMockServers(request, ['xtream']);
|
|
const vodFixture = await fetchXtreamVodFixture(request, portalA);
|
|
const [movieTitle] = pickDistinctTitles(
|
|
vodFixture.items,
|
|
getXtreamTitle
|
|
);
|
|
const app = await launchElectronApp(dataDir);
|
|
|
|
try {
|
|
await addXtreamPortal(app.mainWindow, {
|
|
...portalA,
|
|
name: 'Multi Source A',
|
|
});
|
|
await waitForXtreamWorkspaceReady(app.mainWindow);
|
|
|
|
// The second portal is what makes an alternative exist at all.
|
|
await addXtreamPortal(app.mainWindow, {
|
|
...portalB,
|
|
name: 'Multi Source B',
|
|
});
|
|
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\/[^/]+\/[^/]+$/
|
|
);
|
|
|
|
// Discovery is lazy, so the chip appears after the detail view.
|
|
const chip = app.mainWindow.locator('app-vod-sources-chip button');
|
|
await expect(chip).toBeVisible({ timeout: 20000 });
|
|
// The badge counts every copy across all playlists — the route's
|
|
// own plus the same film in the other portal.
|
|
await expect(chip).toContainText('2');
|
|
|
|
await chip.click();
|
|
|
|
// Both the playing source and the alternative are listed.
|
|
const rows = app.mainWindow.locator('app-vod-source-row');
|
|
await expect(rows).toHaveCount(2);
|
|
// Portal B was added last and is active, so A is the alternative.
|
|
await expect(
|
|
rows.filter({ hasText: 'Multi Source A' })
|
|
).toHaveCount(1);
|
|
// The playing row must name its playlist, never a raw UUID.
|
|
await expect(
|
|
rows.filter({ hasText: 'Multi Source B' })
|
|
).toHaveCount(1);
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
|
|
test('shows no sources chip when only one playlist has the movie', async ({
|
|
dataDir,
|
|
request,
|
|
}) => {
|
|
await resetMockServers(request, ['xtream']);
|
|
const vodFixture = await fetchXtreamVodFixture(request, portalA);
|
|
const [movieTitle] = pickDistinctTitles(
|
|
vodFixture.items,
|
|
getXtreamTitle
|
|
);
|
|
const app = await launchElectronApp(dataDir);
|
|
|
|
try {
|
|
await addXtreamPortal(app.mainWindow, {
|
|
...portalA,
|
|
name: 'Multi Source A',
|
|
});
|
|
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\/[^/]+\/[^/]+$/
|
|
);
|
|
|
|
// The action row must look exactly as it did before this feature.
|
|
await expect(
|
|
app.mainWindow.locator('.play-btn').first()
|
|
).toBeVisible({ timeout: 20000 });
|
|
await expect(
|
|
app.mainWindow.locator('app-vod-sources-chip')
|
|
).toHaveCount(0);
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
});
|