feat(workspace): move page Back buttons into the header and add a history fallback (#1814)

This commit is contained in:
4gray authored and GitHub committed 2026-10-04 12:16:39 +02:00
1 parent 6f247fb538
commit 84aef83a6c
40 files changed
+902 -216

No files matched your search

@@ -24,6 +24,11 @@ import {
// The "Episodes" heading must also stay on one line: the detail pane is far
// narrower than the window beside the rail and category panel, and the
// heading used to wrap beside its actions.
//
// Pages without a Back of their own (the list a detail returns to) get the
// 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.
// ---------------------------------------------------------------------------
const widths = [1280, 780, 375];
@@ -45,6 +50,16 @@ function headerBack(page: Page): Locator {
return page.getByTestId('workspace-header-back');
}
/**
* The generic history Back. A detail's own Back advertises Escape in browse;
* the fallback runs no page handler, so it advertises none.
*/
async function expectHistoryBack(page: Page): Promise<void> {
await expect(headerBack(page)).toBeVisible();
await expect(headerBack(page)).toHaveAccessibleName('Back');
await expect(headerBack(page)).not.toHaveAttribute('aria-keyshortcuts');
}
/** Line boxes of the heading's text; 1 means it did not wrap. */
function headingLineCount(page: Page): Promise<number> {
return page
@@ -228,12 +243,15 @@ async function startFirstEpisode(page: Page): Promise<void> {
).toBeVisible({ timeout: 20_000 });
}
/** The header Back leaves the detail and is gone from the list it opens. */
/**
* The header Back leaves the detail; the list it opens keeps only the
* history fallback (it was itself reached by navigation).
*/
async function expectHeaderBackReturnsToList(page: Page): Promise<void> {
await headerBack(page).click();
await expect(page).not.toHaveURL(detailUrlPattern);
await expect(page.locator('app-portal-detail-shell')).toHaveCount(0);
await expect(headerBack(page)).toHaveCount(0);
await expectHistoryBack(page);
}
test.describe('Portal detail header Back', () => {
@@ -248,7 +266,7 @@ test.describe('Portal detail header Back', () => {
const page = app.mainWindow;
await addXtreamPortal(page);
await waitForXtreamWorkspaceReady(page);
await expect(headerBack(page)).toHaveCount(0);
await expectHistoryBack(page);
const detailUrl = await openFirstSeries(page);
await expectBackInHeader(page, 'browse');
@@ -287,4 +305,45 @@ test.describe('Portal detail header Back', () => {
await closeElectronApp(app);
}
});
test('@xtream @electron falls back to history where no page offers Back', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['xtream']);
const app = await launchElectronApp(dataDir);
try {
const page = app.mainWindow;
await page.waitForURL(/\/workspace\//);
const startUrl = page.url();
// The first page of the session has nowhere to go back to: the
// slot is empty, not a disabled arrow.
await expect(headerBack(page)).toHaveCount(0);
await addXtreamPortal(page);
await waitForXtreamWorkspaceReady(page);
const listUrl = page.url();
await expectHistoryBack(page);
// On a phone the list's drawer toggle keeps the slot: it is the
// only way into the categories.
await page.setViewportSize({ width: 375, height: 800 });
await expect(page.getByTestId('context-drawer-toggle')).toBeVisible();
await expect(headerBack(page)).toBeHidden();
await page.setViewportSize({ width: widths[0], height: 800 });
await headerBack(page).click();
await expect(page).toHaveURL(startUrl);
await expect(headerBack(page)).toHaveCount(0);
// Forward history is not offered; browser Forward still works and
// brings the fallback back.
await page.goForward();
await expect(page).toHaveURL(listUrl);
await expectHistoryBack(page);
} finally {
await closeElectronApp(app);
}
});
});
@@ -638,6 +638,18 @@ test.describe('Electron Workspace Search', () => {
await expect(
xtreamSearchResultCards(app.mainWindow).first()
).toBeVisible({ timeout: 20000 });
// The search page's Back is the header's leading button, not an
// arrow beside its title, and it returns to the dashboard.
const headerBack = app.mainWindow.getByTestId(
'workspace-header-back'
);
await expect(headerBack).toBeVisible();
await expect(
app.mainWindow.getByRole('button', { name: 'Back', exact: true })
).toHaveCount(1);
await headerBack.click();
await expectPathname(app.mainWindow, /\/workspace\/dashboard$/);
} finally {
await closeElectronApp(app);
}