Reported from the first device run: back does not navigate back in the app. The scaffold's `MainActivity.onBackPressed` asks `webView.canGoBack()` and finishes the activity otherwise -- and this app had never touched `history`, so that was false at every depth and back quit from anywhere. The fix is here rather than in Java, because the mechanism the scaffold already uses is the one we were failing to feed: a navigation is a history entry now, and `popstate` replays it. Nothing on the Android side changes, and the behaviour becomes assertable in a browser with `page.goBack()` instead of only on a phone. The entry keeps the same URL -- the app has no routes, and a path a reload cannot resolve is worse than none -- and carries the destination in its state. Two rules keep the stacks from disagreeing. The first navigation *replaces* the launch entry rather than pushing one, or every launch costs a back press before the app will close. And the in-app back buttons go through `history.back()` rather than popping a stack of their own: `navStack` is deleted, not kept alongside, because two stacks is precisely how a detail view's own button and the phone's gesture come to disagree about how far one press goes. The third spec pins that invariant.
93 lines
3.3 KiB
TypeScript
93 lines
3.3 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.
|
|
*/
|
|
type Page = import('@playwright/test').Page;
|
|
|
|
const activeView = (page: Page) =>
|
|
page.getByTestId('main-content');
|
|
|
|
/**
|
|
* 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('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');
|
|
});
|
|
});
|