mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(workspace): move page Back buttons into the header and add a history fallback (#1814)
This commit is contained in:
1 parent
6f247fb538
commit
84aef83a6c
40 files changed
+902
-216
No files matched your search
@@ -406,17 +406,21 @@ test('@web @m3u @tmdb browse and watch keep the adjusted volume', async ({
|
||||
)
|
||||
)
|
||||
.toBe(0.25);
|
||||
// M3U has no browse Back target, so the header shows no arrow in either
|
||||
// state; the now-playing bar's own Close button returns to browse.
|
||||
// M3U registers no Back target in either state: the header's only arrow
|
||||
// is the history fallback to the dashboard the import started from,
|
||||
// which claims no Escape. The now-playing bar's own Close button
|
||||
// returns to browse.
|
||||
const shell = detail(page).locator('app-portal-detail-shell');
|
||||
const headerBack = page.locator('[data-test-id="workspace-header-back"]');
|
||||
await expect(headerBack).toHaveCount(0);
|
||||
await expect(headerBack).toHaveCount(1);
|
||||
await expect(headerBack).not.toHaveAttribute('aria-keyshortcuts');
|
||||
await shell
|
||||
.locator('app-portal-inline-player')
|
||||
.getByRole('button', { name: 'Close player', exact: true })
|
||||
.click();
|
||||
await expect(inlineVideo(page)).toHaveCount(0);
|
||||
await expect(headerBack).toHaveCount(0);
|
||||
await expect(headerBack).toHaveCount(1);
|
||||
await expect(headerBack).not.toHaveAttribute('aria-keyshortcuts');
|
||||
// The hero keeps its own inset (32px, or 20px in a pane narrower than
|
||||
// 760px) in both states.
|
||||
expect(
|
||||
|
||||
@@ -21,8 +21,9 @@ import {
|
||||
* by default so the content keeps the full viewport width, opened from
|
||||
* the header toggle (winning over the persisted desktop inline width),
|
||||
* and closed again by picking a category or tapping the backdrop.
|
||||
* 4. The settings section list scrolls instead of painting over the
|
||||
* Back footer — now inside the open drawer.
|
||||
* 4. Settings keeps its drawer toggle beside the header Back (the drawer
|
||||
* holds the sections), and the section list scrolls inside the drawer.
|
||||
* On a portal list, the header's history Back yields to the toggle.
|
||||
* 5. On a 640x360 landscape phone the live route keeps the channel
|
||||
* sidebar at least 72px tall and the player container inside the
|
||||
* viewport.
|
||||
@@ -107,17 +108,25 @@ test.describe('portrait phone 375x812', () => {
|
||||
await expectRailLinksInsideTopBar(page);
|
||||
});
|
||||
|
||||
test('@mobile settings drawer opens from the header toggle and keeps the section list clear of the Back footer', async ({
|
||||
test('@mobile settings keeps the drawer toggle beside the header Back and the section list inside the drawer', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/workspace/settings');
|
||||
|
||||
// Back is the header's leading button. The drawer holds the section
|
||||
// list, so its toggle stays beside Back instead of giving way.
|
||||
const back = page.locator('[data-test-id="workspace-header-back"]');
|
||||
const toggle = page.locator('[data-test-id="context-drawer-toggle"]');
|
||||
await expect(back).toBeVisible();
|
||||
await expect(toggle).toBeVisible();
|
||||
expect((await boxOf(back)).x).toBeLessThan((await boxOf(toggle)).x);
|
||||
|
||||
// The phone context panel is an off-canvas drawer: hidden until the
|
||||
// header toggle opens it, so the settings content owns the pane.
|
||||
const panel = page.locator('.context-panel--settings');
|
||||
await expect(panel).toBeHidden();
|
||||
|
||||
await page.locator('[data-test-id="context-drawer-toggle"]').click();
|
||||
await toggle.click();
|
||||
await expect(panel).toBeVisible();
|
||||
|
||||
// Narrower than the viewport so the backdrop stays tappable, and
|
||||
@@ -126,15 +135,13 @@ test.describe('portrait phone 375x812', () => {
|
||||
expect(panelBox.width).toBeGreaterThanOrEqual(300);
|
||||
expect(panelBox.width).toBeLessThanOrEqual(PHONE.width - 20);
|
||||
|
||||
const footer = panel.locator('.settings-panel-footer');
|
||||
await expect(footer.locator('.settings-back-button')).toBeVisible();
|
||||
|
||||
// Before #1326 the section list kept its full content height and
|
||||
// painted over the footer whenever the panel was shorter than its
|
||||
// sections; now the list scrolls and ends above the footer.
|
||||
// No footer Back any more; the list scrolls and ends inside the
|
||||
// panel instead of painting past it (#1326).
|
||||
await expect(panel.locator('button:has-text("Back")')).toHaveCount(0);
|
||||
const listBox = await boxOf(panel.locator('.settings-sections-list'));
|
||||
const footerBox = await boxOf(footer);
|
||||
expect(listBox.y + listBox.height).toBeLessThanOrEqual(footerBox.y + 1);
|
||||
expect(listBox.y + listBox.height).toBeLessThanOrEqual(
|
||||
panelBox.y + panelBox.height + 1
|
||||
);
|
||||
|
||||
// Tapping the backdrop (right of the drawer) closes it.
|
||||
await page
|
||||
@@ -163,7 +170,18 @@ test.describe('xtream portal routes on a phone', () => {
|
||||
test('@mobile @xtream vod route keeps the context panel in a drawer behind the header toggle', async ({
|
||||
page,
|
||||
}) => {
|
||||
// The import navigated here from the dashboard, so the header offers
|
||||
// the history Back on wide screens...
|
||||
const back = page.locator('[data-test-id="workspace-header-back"]');
|
||||
await expect(back).toBeVisible();
|
||||
|
||||
await page.setViewportSize(PHONE);
|
||||
// ...and on a phone it yields to the drawer toggle, the list's only
|
||||
// way into its categories.
|
||||
await expect(back).toBeHidden();
|
||||
await expect(
|
||||
page.locator('[data-test-id="context-drawer-toggle"]')
|
||||
).toBeVisible();
|
||||
|
||||
// Hidden by default — the content owns the full pane. This is the
|
||||
// successor to the #1326 stacked layout, which left the content
|
||||
|
||||
@@ -7,7 +7,12 @@ async function openSettings(page: Page) {
|
||||
// The bare settings URL redirects to the default section page.
|
||||
await page.waitForURL(/\/workspace\/settings\/general$/);
|
||||
await expect(page.locator('.settings-container')).toBeVisible();
|
||||
await expect(page.locator('.settings-back-button')).toBeVisible();
|
||||
await expect(settingsBack(page)).toBeVisible();
|
||||
}
|
||||
|
||||
/** Settings' Back is the workspace header's leading button. */
|
||||
function settingsBack(page: Page) {
|
||||
return page.locator('[data-test-id="workspace-header-back"]');
|
||||
}
|
||||
|
||||
/** Settings render one section page at a time — open it via the rail. */
|
||||
@@ -36,7 +41,12 @@ test.describe('Settings', () => {
|
||||
|
||||
test('@settings @web Check settings page', async ({ page }) => {
|
||||
await openSettings(page);
|
||||
await page.locator('.settings-back-button').click();
|
||||
// The context panel no longer carries a Back of its own.
|
||||
await expect(
|
||||
page.locator('app-workspace-settings-context-panel button')
|
||||
).toHaveCount(0);
|
||||
await settingsBack(page).click();
|
||||
await page.waitForURL(/\/workspace\/dashboard$/);
|
||||
});
|
||||
|
||||
test('@settings @web Change video player', async ({ page }) => {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from './fixtures';
|
||||
import {
|
||||
addXtreamPortal,
|
||||
interceptXtreamRequests,
|
||||
MOCK_SERVER,
|
||||
} from './xtream-series-playback.fixture';
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Contract: docs/architecture/workspace-shell.md, "Header Back".
|
||||
*/
|
||||
|
||||
const headerBack = (page: Page) =>
|
||||
page.locator(
|
||||
'app-workspace-shell-header [data-test-id="workspace-header-back"]'
|
||||
);
|
||||
|
||||
/** The one Back button on the page, wherever it lives. */
|
||||
const anyBack = (page: Page) =>
|
||||
page.getByRole('button', { name: 'Back', exact: true });
|
||||
|
||||
async function expectOnlyHeaderBack(page: Page): Promise<void> {
|
||||
await expect(headerBack(page)).toBeVisible();
|
||||
await expect(anyBack(page)).toHaveCount(1);
|
||||
// None of these pages handles Escape.
|
||||
await expect(headerBack(page)).not.toHaveAttribute('aria-keyshortcuts');
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
await request.post(`${MOCK_SERVER}/reset`);
|
||||
await page.goto('/');
|
||||
await interceptXtreamRequests(page);
|
||||
await addXtreamPortal(page);
|
||||
});
|
||||
|
||||
test('@web @xtream the in-portal search page returns through the header Back', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page
|
||||
.locator('app-workspace-shell-rail a[href$="/workspace/dashboard"]')
|
||||
.first()
|
||||
.click();
|
||||
await page.waitForURL(/\/workspace\/dashboard$/);
|
||||
// The rail link's tooltip would otherwise sit over the header's leading
|
||||
// button for as long as the pointer rests on the link.
|
||||
await page.mouse.move(640, 400);
|
||||
|
||||
// Enter on the dashboard opens the active portal's search page.
|
||||
const search = page.locator(
|
||||
'app-workspace-shell-header .search-field input[type="search"]'
|
||||
);
|
||||
await search.fill('Movie');
|
||||
await search.press('Enter');
|
||||
await page.waitForURL(/\/workspace\/xtreams\/[^/]+\/search\?q=Movie$/);
|
||||
await expect(page.locator('app-search-layout')).toBeVisible();
|
||||
|
||||
await expectOnlyHeaderBack(page);
|
||||
await headerBack(page).click();
|
||||
await page.waitForURL(/\/workspace\/dashboard$/);
|
||||
});
|
||||
|
||||
for (const { name, path, selector } of [
|
||||
{
|
||||
name: 'Discover',
|
||||
path: 'discover?type=movie&genre=18&genreLabel=Drama',
|
||||
selector: 'app-discover-view',
|
||||
},
|
||||
{ name: 'actor', path: 'actor/287', selector: 'app-actor-view' },
|
||||
]) {
|
||||
test(`@web @xtream the ${name} page returns through the header Back`, async ({
|
||||
page,
|
||||
}) => {
|
||||
const listUrl = page.url();
|
||||
const portalUrl = listUrl.replace(/\/vod.*$/, '');
|
||||
|
||||
await page.goto(`${portalUrl}/${path}`);
|
||||
await expect(page.locator(selector)).toBeAttached();
|
||||
|
||||
await expectOnlyHeaderBack(page);
|
||||
await headerBack(page).click();
|
||||
await page.waitForURL(listUrl);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user