`back-navigation.spec.ts` covered exactly the journeys #72 breaks and was green throughout it, because every assertion in it was `data-active-view` — which the shell sets on every path including the back one, and which was the one thing already correct. The same trap `layout-overflow.spec.ts` set for #69: a spec named for the behaviour, measuring the plumbing. The assertions go here rather than in a second file, or the first would carry on passing vacuously. They are `aria-current="page"` through `getByRole`, which is the accessible fact — `.active` is a class and could be restyled without breaking anything real — and the role query resolves to whichever nav is in the accessibility tree at that viewport, so one helper covers the sidebar and the tab bar. Three of the four fail on the build before the fix. The fourth, the parent staying lit while a detail view is open, passed by accident and says so.
229 lines
8.7 KiB
TypeScript
229 lines
8.7 KiB
TypeScript
import { test, expect } from '../support/fixtures.js';
|
|
|
|
/**
|
|
* Back is the platform's, and the app has to have somewhere for it to
|
|
* go (reported from a device: "the Android back button does not
|
|
* navigate back in the app").
|
|
*
|
|
* The scaffold's `MainActivity.onBackPressed` asks `webView.canGoBack()`
|
|
* and finishes the activity otherwise. This app never touched
|
|
* `history`, so that was always false and back quit from any depth. A
|
|
* navigation is a history entry now, which is why this is assertable
|
|
* here at all: `page.goBack()` is the same `popstate` the phone's
|
|
* gesture produces, so the browser tier can answer a question that
|
|
* otherwise needs a device.
|
|
*
|
|
* What it cannot answer is whether Android's *gesture* reaches the
|
|
* WebView, which is between the OS and the scaffold.
|
|
*
|
|
* **And `data-active-view` is not the behaviour.** Every assertion here
|
|
* used to be that attribute, which the shell sets on every path
|
|
* including `_isBack` — so this file was green throughout #72, in
|
|
* which both navs highlighted the view the user had just *left*. The
|
|
* shell's own bookkeeping was the one thing that was already right;
|
|
* what a person sees is `aria-current`, and that is asserted below as
|
|
* well. This is the same trap `layout-overflow.spec.ts` set for #69: a
|
|
* spec named for the behaviour, measuring the plumbing.
|
|
*/
|
|
type Page = import('@playwright/test').Page;
|
|
|
|
const activeView = (page: Page) =>
|
|
page.getByTestId('main-content');
|
|
|
|
/** A common phone, where the bottom bar is the primary navigation. */
|
|
const PHONE = { width: 390, height: 844 };
|
|
|
|
/**
|
|
* The nav item for a destination, in whichever navigation is on screen.
|
|
*
|
|
* Both navs carry a button named `Albums`, and only one of them is ever
|
|
* in the accessibility tree — the other is `display: none` — so the
|
|
* role query resolves to the one the user can see at this viewport.
|
|
* That is the point: the highlight has to be right in both, and #72 was
|
|
* two different-looking symptoms of one cause.
|
|
*/
|
|
const navItem = (page: Page, label: string) =>
|
|
page.getByRole('button', { name: label, exact: true });
|
|
|
|
/**
|
|
* `aria-current="page"` is the accessible fact and the assertion worth
|
|
* making; `.active` is a class and could be restyled without breaking
|
|
* anything real.
|
|
*/
|
|
async function expectHighlighted(page: Page, label: string): Promise<void> {
|
|
await expect(navItem(page, label)).toHaveAttribute('aria-current', 'page');
|
|
}
|
|
|
|
async function expectNotHighlighted(page: Page, label: string): Promise<void> {
|
|
await expect(navItem(page, label)).toHaveAttribute('aria-current', 'false');
|
|
}
|
|
|
|
/**
|
|
* Open an artist's detail view, which is the deepest ordinary route.
|
|
*
|
|
* A library artist opens `explore-artist-details` -- the catalog panel
|
|
* standing in for a library one, as `explore-link.ts` describes -- and
|
|
* the view name follows the component, not the source of the click.
|
|
*/
|
|
async function openAnArtist(app: Page): Promise<void> {
|
|
await app.getByTestId('nav-artists').click();
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'artists');
|
|
|
|
// A card, by the name on it: the grid is virtualized and positioned
|
|
// by transform, so a click at coordinates is a click at whatever
|
|
// happens to be there.
|
|
await app.locator('artists-view').getByText('Aurora Fields').first().click();
|
|
await expect(activeView(app)).toHaveAttribute(
|
|
'data-active-view',
|
|
'explore-artist-details',
|
|
);
|
|
}
|
|
|
|
test.describe('the back gesture', () => {
|
|
test('leaves a detail view for the view it was opened from', async ({
|
|
app,
|
|
}) => {
|
|
await openAnArtist(app);
|
|
|
|
await app.goBack();
|
|
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'artists');
|
|
});
|
|
|
|
test('walks back through primary views, one press per navigation', async ({
|
|
app,
|
|
}) => {
|
|
await app.getByTestId('nav-tracks').click();
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'tracks');
|
|
|
|
await app.getByTestId('nav-albums').click();
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');
|
|
|
|
await app.goBack();
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'tracks');
|
|
|
|
// Forward is free once back works, and it is what proves the entry
|
|
// was restored rather than the view merely re-rendered.
|
|
await app.goForward();
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');
|
|
});
|
|
|
|
test('leaves the nav highlighting the view it landed on, not the one it left', async ({
|
|
app,
|
|
}) => {
|
|
await app.getByTestId('nav-albums').click();
|
|
await expectHighlighted(app, 'Albums');
|
|
|
|
await app.getByTestId('nav-tracks').click();
|
|
await expectHighlighted(app, 'Tracks');
|
|
|
|
await app.goBack();
|
|
|
|
// #72, and the half of it the report did not describe: this is
|
|
// desktop, and before the shell published the active view *both*
|
|
// navs stayed on Tracks. An absent highlight reads as a glitch; a
|
|
// confident wrong one is worse, and any back across two primary
|
|
// views produced it.
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');
|
|
await expectHighlighted(app, 'Albums');
|
|
await expectNotHighlighted(app, 'Tracks');
|
|
});
|
|
|
|
test('keeps the parent destination lit while a detail view is open', async ({
|
|
app,
|
|
}) => {
|
|
await app.getByTestId('nav-artists').click();
|
|
await expectHighlighted(app, 'Artists');
|
|
|
|
await openAnArtist(app);
|
|
|
|
// A detail view is not a destination in either nav, and the user is
|
|
// still inside Artists. `app-sidebar` did this by accident -- it
|
|
// guarded on its own item list, so an unmatched name left the
|
|
// highlight alone -- and that accident is why the sidebar looked
|
|
// right on a detail view while the tab bar lit nothing. This test
|
|
// therefore passed before the fix and is here to keep the rule from
|
|
// being lost while the others are made to pass; the *tab bar's*
|
|
// half of it is the phone test below, which did not.
|
|
await expectHighlighted(app, 'Artists');
|
|
|
|
await app.goBack();
|
|
|
|
await expectHighlighted(app, 'Artists');
|
|
});
|
|
|
|
test('the tab bar survives the same journey on a phone', async ({ app }) => {
|
|
await app.setViewportSize(PHONE);
|
|
|
|
// The reported shape: Albums, open an album, press back. The tab
|
|
// bar had a highlight, then no highlight at all, and never got it
|
|
// back — `bottom-nav` took the detail view's name, matched it
|
|
// against no tab, and lit nothing.
|
|
await navItem(app, 'Albums').click();
|
|
await expectHighlighted(app, 'Albums');
|
|
|
|
await app.locator('cover-grid').getByText('Glass Harbour').first().click();
|
|
await expect(activeView(app)).toHaveAttribute(
|
|
'data-active-view',
|
|
'explore-album-details',
|
|
);
|
|
await expectHighlighted(app, 'Albums');
|
|
|
|
await app.goBack();
|
|
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'albums');
|
|
await expectHighlighted(app, 'Albums');
|
|
});
|
|
|
|
test('the drawer sidebar opens on the page you are standing on', async ({
|
|
app,
|
|
}) => {
|
|
await app.setViewportSize(PHONE);
|
|
|
|
await navItem(app, 'Tracks').click();
|
|
await expectHighlighted(app, 'Tracks');
|
|
|
|
// A third symptom of the same cause, found while measuring #72 and
|
|
// not in the report: `bottom-nav` mounts its `<app-sidebar>` when
|
|
// the drawer opens, so that copy had heard no `navigate` at all and
|
|
// showed its own default — Home, from any page in the app. An event
|
|
// has no answer for a listener that was not there; a store does.
|
|
await navItem(app, 'More').click();
|
|
|
|
// The element carrying the testid is the `wa-drawer` host, which
|
|
// always reports hidden -- what is visible is the `<dialog>` in its
|
|
// shadow root -- so the drawer being open is asserted of the
|
|
// sidebar it holds rather than of itself.
|
|
const drawer = app.getByTestId('nav-drawer');
|
|
|
|
await expect(drawer.locator('app-sidebar')).toBeVisible();
|
|
await expect(drawer.getByTestId('nav-tracks')).toHaveAttribute(
|
|
'aria-current',
|
|
'page',
|
|
);
|
|
await expect(drawer.getByTestId('nav-home')).toHaveAttribute(
|
|
'aria-current',
|
|
'false',
|
|
);
|
|
});
|
|
|
|
test('an in-app back button consumes exactly one entry', async ({ app }) => {
|
|
await app.getByTestId('nav-tracks').click();
|
|
await openAnArtist(app);
|
|
|
|
// The detail view's own back button and the phone's gesture are the
|
|
// same press: if each popped its own stack, this would land two
|
|
// navigations back instead of one.
|
|
await app
|
|
.locator('explore-artist-details')
|
|
.getByRole('button', { name: 'Back to explore' })
|
|
.click();
|
|
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'artists');
|
|
|
|
await app.goBack();
|
|
|
|
await expect(activeView(app)).toHaveAttribute('data-active-view', 'tracks');
|
|
});
|
|
});
|