feat: preserve focus and catalog context during public navigation
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-10 07:53:03 -04:00
parent a02965de4a
commit 73c430b8a5
15 changed files with 175 additions and 12 deletions
@@ -0,0 +1,77 @@
import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.route('**/api/v1/public/**', async (route) => {
const path = new URL(route.request().url()).pathname;
if (path.includes('/lookups/')) return route.fulfill({ json: { data: [] } });
if (path.endsWith('/search')) return route.fulfill({ json: { data: {
items: [{ id: 'listing-1', productName: 'Pikachu ex', askingPriceMinor: 2500, status: 'AVAILABLE' }],
pagination: { page: 1, limit: 20, total: 1, totalPages: 1 },
} } });
if (path.endsWith('/missing')) return route.fulfill({ status: 404, json: {} });
return route.fulfill({ json: { data: {
id: 'listing-1', name: 'Pikachu ex', productName: 'Pikachu ex', askingPriceMinor: 2500,
status: 'AVAILABLE', images: [], sellerDisplayName: 'Pika Collectibles',
sellerContactEmail: 'collector@example.test',
} } });
});
});
test('keyboard navigation retains catalog filters, titles and entry focus', async ({ page }) => {
await page.goto('/?q=Pikachu&status=AVAILABLE');
await expect(page).toHaveTitle('Catalog | Pokémon Inventory');
await expect(page.getByRole('main')).toBeFocused();
const search = page.getByRole('searchbox', { name: 'Search the catalog' });
await page.keyboard.press('Tab');
await expect(search).toBeFocused();
await search.fill('Pikachu ex');
await expect(page).toHaveURL(/q=Pikachu(?:\+|%20)ex/);
await expect(search).toBeFocused();
const catalogUrl = page.url();
const card = page.getByRole('link', { name: /Pikachu ex/ });
await card.focus();
await page.keyboard.press('Enter');
await expect(page).toHaveTitle('Pikachu ex | Pokémon Inventory');
await expect(page.getByRole('main')).toBeFocused();
await page.keyboard.press('Tab');
const back = page.getByRole('link', { name: 'Back to catalog' });
await expect(back).toBeFocused();
await page.keyboard.press('Enter');
await expect(page).toHaveURL(catalogUrl);
await expect(search).toHaveValue('Pikachu ex');
await expect(page.getByRole('main')).toBeFocused();
await page.goBack();
await expect(page).toHaveTitle('Pikachu ex | Pokémon Inventory');
await expect(page.getByRole('main')).toBeFocused();
await page.goBack();
await expect(page).toHaveURL(catalogUrl);
await expect(search).toHaveValue('Pikachu ex');
});
test('direct product and unavailable pages have titles, focus and safe return links', async ({ page }) => {
await page.goto('/products/pikachu?from=https://example.test');
await expect(page).toHaveTitle('Pikachu ex | Pokémon Inventory');
await expect(page.getByRole('main')).toBeFocused();
await expect(page.getByRole('link', { name: 'Back to catalog' })).toHaveAttribute('href', '/');
await page.goto('/listings/missing');
await expect(page).toHaveTitle('Listing not found. | Pokémon Inventory');
await expect(page.getByRole('alert')).toHaveText('Listing not found.');
await expect(page.getByRole('main')).toBeFocused();
});
test('late detail results do not steal focus from the return link', async ({ page }) => {
let release;
const responseReady = new Promise((resolve) => { release = resolve; });
await page.route('**/api/v1/public/products/slow', async (route) => {
await responseReady;
await route.fulfill({ json: { data: { name: 'Slow Pikachu', images: [] } } });
});
await page.goto('/products/slow');
await expect(page.getByRole('status')).toHaveText('Loading product...');
await page.keyboard.press('Tab');
const back = page.getByRole('link', { name: 'Back to catalog' });
await expect(back).toBeFocused();
release();
await expect(page).toHaveTitle('Slow Pikachu | Pokémon Inventory');
await expect(back).toBeFocused();
});