mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(workspace): lead header Back to a parent route when the page opened the session (#1830)
* fix(workspace): lead header Back to a parent route when the page opened the session Settings, Discover, actor and in-portal search registered a header Back that only ran Location.back(). As the first entry of the session (deep link, reload, restored view) that did nothing in Electron and left the app in a browser. WorkspaceBackNavigationService.back(resolveParent) keeps Location.back() while the previous entry is an in-app one, and while that is unknown because the Navigation API is missing. Otherwise it opens the page's parent with replaceUrl, so history Back cannot return to the page: - Settings: the first workspace view (resolveDashboardPath()). - Discover: the catalog section it lists (vod for movies, series for TV). - Actor and search: the portal root, which redirects to its default section within the same navigation. The web E2E opens these pages in a fresh tab: a page.goto in the same tab leaves the previous document behind, often at the parent's URL, so history Back passed without the fix. Electron covers settings after a window reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): lead first-entry Back to the parent without the Navigation API Review follow-ups (Greptile): - Without the Navigation API (older Safari and Firefox) back() always called Location.back(), so a page that opened the session still left the app. The service now tracks the router's in-app history depth there (trackRouterHistoryDepth): first navigation 0, push +1, replacement keeps it, a traversal restores the depth recorded for its entry. Depth 0 opens the parent; an unknown depth (an entry from before a reload) keeps Location.back(). - Stalker's Discover (movie/tv section), actor and search pages now have tests that they hand the service the parent under the portal :id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): adopt the router navigation the Back depth tracker missed Review follow-up (Codex, Greptile): the lazy workspace shell creates the Back service after the first NavigationStart, so the tracker saw only its NavigationEnd, left the depth unknown and counted the next push as the first entry. It now adopts the router's current or last successful navigation when it starts: a first navigation is depth 0, a later one leaves the depth unknown (browser history Back), and a late start of the adopted navigation is not counted again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
23 files changed
+940
-55
No files matched your search
@@ -13,6 +13,10 @@ import {
|
||||
test,
|
||||
waitForXtreamWorkspaceReady,
|
||||
} from './electron-test-fixtures';
|
||||
import {
|
||||
expectRendererReloadedOnRoute,
|
||||
reloadFromMainProcess,
|
||||
} from './renderer-reload.support';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A detail page's Back lives in the workspace header's leading slot, not in
|
||||
@@ -29,6 +33,10 @@ import {
|
||||
// header's history fallback while an in-app previous page exists; on a
|
||||
// phone it yields to the drawer toggle, and with nowhere to go the slot is
|
||||
// empty rather than a disabled arrow.
|
||||
//
|
||||
// A page with a parent route (settings here) that opened the session, as
|
||||
// after a reload, has no in-app entry for history Back: its Back leads to
|
||||
// the parent instead and replaces the page's entry.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const widths = [1280, 780, 375];
|
||||
@@ -346,4 +354,33 @@ test.describe('Portal detail header Back', () => {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('@electron @settings settings Back after a reload leads to the dashboard', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
const page = app.mainWindow;
|
||||
await page.waitForURL(/\/workspace\/dashboard$/);
|
||||
await openSettings(page);
|
||||
|
||||
// The reloaded document re-boots on the settings route; the
|
||||
// dashboard entry before it belongs to the old document.
|
||||
await reloadFromMainProcess(app);
|
||||
await expectRendererReloadedOnRoute(
|
||||
page,
|
||||
/\/workspace\/settings\/general$/
|
||||
);
|
||||
await expect(page.getByTestId('settings-container')).toBeVisible();
|
||||
|
||||
await headerBack(page).click();
|
||||
await expect(page).toHaveURL(/\/workspace\/dashboard$/);
|
||||
// The dashboard replaced the settings entry: nothing in this
|
||||
// document precedes it, so the header offers no Back.
|
||||
await expect(headerBack(page)).toHaveCount(0);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -49,6 +49,24 @@ test.describe('Settings', () => {
|
||||
await page.waitForURL(/\/workspace\/dashboard$/);
|
||||
});
|
||||
|
||||
test('@settings @web settings opening the session lead to the dashboard', async ({
|
||||
page,
|
||||
}) => {
|
||||
// The only entry of a new tab's history, as in Electron after a
|
||||
// deep link: browser Back has nowhere to go.
|
||||
const firstEntryPage = await page.context().newPage();
|
||||
await firstEntryPage.goto('/workspace/settings/general');
|
||||
// A cold start in a new tab passes the startup splash first.
|
||||
await expect(firstEntryPage.locator('.settings-container')).toBeVisible(
|
||||
{ timeout: 15_000 }
|
||||
);
|
||||
|
||||
await settingsBack(firstEntryPage).click();
|
||||
await firstEntryPage.waitForURL(/\/workspace\/dashboard$/);
|
||||
// The dashboard replaced the settings entry: no Back leads to it.
|
||||
await expect(settingsBack(firstEntryPage)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('@settings @web Change video player', async ({ page }) => {
|
||||
await openSettings(page);
|
||||
await openSettingsSection(page, 'playback');
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
|
||||
/**
|
||||
* Pages reached from a detail or the header search draw no Back arrow of
|
||||
* their own: they register the workspace header's leading Back, which keeps
|
||||
* their previous return behaviour (history Back).
|
||||
* their own: they register the workspace header's leading Back. It goes back
|
||||
* in history when the page was reached in the app; a page that opened the
|
||||
* session (a deep link, reload or restored view) leads to its parent
|
||||
* instead, replacing its own history entry.
|
||||
* Contract: docs/architecture/workspace-shell.md, "Header Back".
|
||||
*/
|
||||
|
||||
@@ -29,6 +31,24 @@ async function expectOnlyHeaderBack(page: Page): Promise<void> {
|
||||
await expect(headerBack(page)).not.toHaveAttribute('aria-keyshortcuts');
|
||||
}
|
||||
|
||||
/** The portal's own URL, from the VOD list that adding it opens. */
|
||||
function portalUrlOf(page: Page): string {
|
||||
return page.url().replace(/\/vod.*$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens `url` as the only entry of a new tab's history, where browser Back
|
||||
* has nowhere to go, as in Electron after a deep link or restored view. A
|
||||
* `page.goto` in the same tab would leave the previous document behind,
|
||||
* often at the very URL of the parent.
|
||||
*/
|
||||
async function openAsFirstEntry(page: Page, url: string): Promise<Page> {
|
||||
const firstEntryPage = await page.context().newPage();
|
||||
await interceptXtreamRequests(firstEntryPage);
|
||||
await firstEntryPage.goto(url);
|
||||
return firstEntryPage;
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
await request.post(`${MOCK_SERVER}/reset`);
|
||||
await page.goto('/');
|
||||
@@ -36,7 +56,7 @@ test.beforeEach(async ({ page, request }) => {
|
||||
await addXtreamPortal(page);
|
||||
});
|
||||
|
||||
test('@web @xtream the in-portal search page returns through the header Back', async ({
|
||||
test('@web @xtream the in-portal search page reached in the app goes back in history', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page
|
||||
@@ -62,25 +82,51 @@ test('@web @xtream the in-portal search page returns through the header Back', a
|
||||
await page.waitForURL(/\/workspace\/dashboard$/);
|
||||
});
|
||||
|
||||
for (const { name, path, selector } of [
|
||||
for (const { name, path, selector, parent } of [
|
||||
{
|
||||
name: 'Discover',
|
||||
name: 'movie Discover',
|
||||
path: 'discover?type=movie&genre=18&genreLabel=Drama',
|
||||
selector: 'app-discover-view',
|
||||
parent: 'vod',
|
||||
},
|
||||
{
|
||||
name: 'series Discover',
|
||||
path: 'discover?type=tv&genre=18&genreLabel=Drama',
|
||||
selector: 'app-discover-view',
|
||||
parent: 'series',
|
||||
},
|
||||
// The portal root: its route config redirects to the default section.
|
||||
{
|
||||
name: 'actor',
|
||||
path: 'actor/287',
|
||||
selector: 'app-actor-view',
|
||||
parent: 'vod',
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: 'search?q=Movie',
|
||||
selector: 'app-search-layout',
|
||||
parent: 'vod',
|
||||
},
|
||||
{ name: 'actor', path: 'actor/287', selector: 'app-actor-view' },
|
||||
]) {
|
||||
test(`@web @xtream the ${name} page returns through the header Back`, async ({
|
||||
test(`@web @xtream the ${name} page opening the session leads to its parent`, async ({
|
||||
page,
|
||||
}) => {
|
||||
const listUrl = page.url();
|
||||
const portalUrl = listUrl.replace(/\/vod.*$/, '');
|
||||
const portalUrl = portalUrlOf(page);
|
||||
const firstEntryPage = await openAsFirstEntry(
|
||||
page,
|
||||
`${portalUrl}/${path}`
|
||||
);
|
||||
// A cold start in a new tab passes the startup splash first.
|
||||
await expect(firstEntryPage.locator(selector)).toBeAttached({
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
await page.goto(`${portalUrl}/${path}`);
|
||||
await expect(page.locator(selector)).toBeAttached();
|
||||
|
||||
await expectOnlyHeaderBack(page);
|
||||
await headerBack(page).click();
|
||||
await page.waitForURL(listUrl);
|
||||
await expectOnlyHeaderBack(firstEntryPage);
|
||||
await headerBack(firstEntryPage).click();
|
||||
await firstEntryPage.waitForURL(`${portalUrl}/${parent}`);
|
||||
// The parent replaced the page's entry, so nothing precedes it and
|
||||
// the list shows no Back (the history fallback would otherwise).
|
||||
await expect(headerBack(firstEntryPage)).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user