From 56977dbe0561df45634c45b0c0242f929169912e Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Sun, 25 Oct 2026 17:00:35 +0200 Subject: [PATCH 1/5] test(frontend): cover the main landmark, the about heading, the boolean filter value, and the sheet close button --- .../features/app-shell/app-shell.locators.ts | 2 ++ .../features/app-shell/app-shell.spec.ts | 19 +++++++++++++++ .../filter-demo/filter-demo.locators.ts | 2 ++ .../features/filter-demo/filter-demo.spec.ts | 24 +++++++++++++++++++ .../features/filter-demo/filter-demo.tasks.ts | 12 ++++++++++ 5 files changed, 59 insertions(+) diff --git a/frontend/tests/features/app-shell/app-shell.locators.ts b/frontend/tests/features/app-shell/app-shell.locators.ts index f2e195d4..77391500 100644 --- a/frontend/tests/features/app-shell/app-shell.locators.ts +++ b/frontend/tests/features/app-shell/app-shell.locators.ts @@ -1,8 +1,10 @@ import type { Page } from '@playwright/test' export const appShell = (page: Page) => ({ + aboutHeading: page.getByRole('heading', { level: 1, name: 'About' }), aboutText: page.getByText('This is a demo application built with .NET Aspire and React'), currentPage: page.getByRole('navigation', { name: 'breadcrumb' }).getByRole('link'), + mainLandmarks: page.getByRole('main'), notFoundHeading: page.getByRole('heading', { name: 'Page not found' }), homeLink: page.getByRole('link', { name: 'Go to the home page' }), weatherHeading: page.getByRole('heading', { name: 'Weather Forecast' }), diff --git a/frontend/tests/features/app-shell/app-shell.spec.ts b/frontend/tests/features/app-shell/app-shell.spec.ts index 6be41163..5959056b 100644 --- a/frontend/tests/features/app-shell/app-shell.spec.ts +++ b/frontend/tests/features/app-shell/app-shell.spec.ts @@ -33,6 +33,25 @@ test.describe('the home page on a desktop', () => { }) }) +test.describe('the landmarks of a page', () => { + test('the home page has one main landmark', async ({ page, api }) => { + api.mock(weatherApi.listReturns(aForecast())) + + await weatherForecastPage(page).open() + + expect(await appShellPage(page).at.mainLandmarks.count()).toBe(1) + }) + + test('the about page has one main landmark and a level 1 heading', async ({ page }) => { + const shell = appShellPage(page) + + await shell.openAbout() + + expect(await shell.at.mainLandmarks.count()).toBe(1) + await expect(shell.at.aboutHeading).toBeVisible() + }) +}) + test.describe('an unknown address', () => { test('shows a page not found message in the app layout', async ({ page }) => { const shell = appShellPage(page) diff --git a/frontend/tests/features/filter-demo/filter-demo.locators.ts b/frontend/tests/features/filter-demo/filter-demo.locators.ts index 425720df..3414f5d7 100644 --- a/frontend/tests/features/filter-demo/filter-demo.locators.ts +++ b/frontend/tests/features/filter-demo/filter-demo.locators.ts @@ -13,9 +13,11 @@ export const filterDemo = (page: Page) => { valueGroup: (propertyLabel: string) => control(propertyLabel).getByRole('group', { name: propertyLabel, exact: true }), controlLabel: (propertyLabel: string) => control(propertyLabel).getByText(propertyLabel, { exact: true }), button: (propertyLabel: string, name: string) => control(propertyLabel).getByRole('button', { name, exact: true }), + pressedButtons: (propertyLabel: string) => control(propertyLabel).getByRole('button', { pressed: true }), dateField: (propertyLabel: string, name: string) => control(propertyLabel).getByRole('group', { name, exact: true }), calendar: (propertyLabel: string) => control(propertyLabel).getByRole('grid'), addFilter: (propertyLabel: string) => control(propertyLabel).getByRole('button', { name: 'Add Filter', exact: true }), + editFilter: (propertyLabel: string) => page.getByRole('button', { name: new RegExp(`^${propertyLabel} \\|`) }), removeFilter: (propertyLabel: string) => page.getByRole('button', { name: new RegExp(`^Remove filter: ${propertyLabel}`) }), } } diff --git a/frontend/tests/features/filter-demo/filter-demo.spec.ts b/frontend/tests/features/filter-demo/filter-demo.spec.ts index 2628569f..97515ba9 100644 --- a/frontend/tests/features/filter-demo/filter-demo.spec.ts +++ b/frontend/tests/features/filter-demo/filter-demo.spec.ts @@ -31,6 +31,15 @@ test.describe('the Add Filter menu on a phone', () => { await expect(demo.at.valueGroup('Is Blocked')).toBeVisible() }) + test('gives the close button of the sheet a 32 px touch target', async ({ page }) => { + const demo = filterDemoPage(page) + await demo.open() + + await demo.chooseProperty('Is Blocked') + + expect(await demo.at.button('Is Blocked', 'Close').boundingBox()).toMatchObject({ width: 32, height: 32 }) + }) + test('adds a date filter from its control', async ({ page }) => { const demo = filterDemoPage(page) await demo.open() @@ -142,6 +151,21 @@ test.describe('the Add Filter menu on a desktop', () => { }) }) +test.describe('the edit dialog of a boolean filter', () => { + for (const [value, other] of [['True', 'False'], ['False', 'True']] as const) { + test(`shows ${value} as the current value`, async ({ page }) => { + const demo = filterDemoPage(page) + await demo.open() + await demo.addBooleanFilter('Is Blocked', value) + + await demo.editFilter('Is Blocked') + + await expect(demo.at.pressedButtons('Is Blocked')).toHaveText([value]) + await expect(demo.at.button('Is Blocked', other)).toHaveAttribute('aria-pressed', 'false') + }) + } +}) + const shortWindow = { width: 1280, height: 512 } test.describe('the Add Filter menu on a short desktop window', () => { diff --git a/frontend/tests/features/filter-demo/filter-demo.tasks.ts b/frontend/tests/features/filter-demo/filter-demo.tasks.ts index 7d41bac1..dc457bcd 100644 --- a/frontend/tests/features/filter-demo/filter-demo.tasks.ts +++ b/frontend/tests/features/filter-demo/filter-demo.tasks.ts @@ -30,6 +30,18 @@ export function filterDemoPage(page: Page) { await expect(at.dateField(propertyLabel, 'Start date')).toBeVisible() }, + async addBooleanFilter(propertyLabel: string, value: 'True' | 'False') { + await at.addFilterMenu.click() + await at.property(propertyLabel).click() + await at.button(propertyLabel, value).click() + await expect(at.control(propertyLabel)).toBeHidden() + }, + + async editFilter(propertyLabel: string) { + await at.editFilter(propertyLabel).click() + await expect(at.control(propertyLabel)).toBeVisible() + }, + async addTodayFilter(propertyLabel: string) { await at.button(propertyLabel, 'Today').click() await at.addFilter(propertyLabel).click() From 1a53cd95f12fd4c61d4cd6a08d30921a9110cefa Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Sun, 25 Oct 2026 17:02:01 +0200 Subject: [PATCH 2/5] fix(frontend): keep one main landmark on Home and About and give About an h1 The layout already renders SidebarInset as the main landmark. Home and About put a second main inside it, so a screen reader listed two nested main landmarks. They now use a div. The About title was a div, so the page had no h1. The title now holds an h1 with the same look. --- frontend/src/routes/about.tsx | 8 +++++--- frontend/src/routes/index.tsx | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes/about.tsx b/frontend/src/routes/about.tsx index 372a9b51..6f23afb0 100644 --- a/frontend/src/routes/about.tsx +++ b/frontend/src/routes/about.tsx @@ -8,10 +8,12 @@ export const Route = createFileRoute('/about')({ function About() { return (
-
+
- About + +

About

+

@@ -45,7 +47,7 @@ function About() { -

+
) } diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 665e4b20..76392d38 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -65,7 +65,7 @@ function Index() { return (
-
+
{/* Header */}
@@ -214,7 +214,7 @@ function Index() {
)} -
+
) } From 9790f379da9f420ec1d087319754f074b8d99640 Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Sun, 25 Oct 2026 17:02:16 +0200 Subject: [PATCH 3/5] fix(frontend): show the current value in the boolean filter edit dialog The Edit Is Blocked Filter dialog showed True and False as two equal outline buttons, so the user could not see which value the filter had. The control now reads initialFilter. The current value gets the filled button style and aria-pressed=true. The Add Filter menu has no current value and does not change. --- .../filter-builder/controls/boolean-filter.tsx | 14 +++++++++----- .../tests/features/filter-demo/filter-demo.spec.ts | 10 ++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/filter-builder/controls/boolean-filter.tsx b/frontend/src/components/filter-builder/controls/boolean-filter.tsx index ff158904..4dd9e636 100644 --- a/frontend/src/components/filter-builder/controls/boolean-filter.tsx +++ b/frontend/src/components/filter-builder/controls/boolean-filter.tsx @@ -3,11 +3,13 @@ import { Button } from '@/components/ui/button' import type { Filter, FilterControlProps } from '../types' import { Operators } from '../utils/operators' -// A click on a value submits, so the control does not read initialFilter. -type BooleanFilterProps = Pick +type BooleanFilterProps = Pick -export function BooleanFilter({ config, onSubmit }: BooleanFilterProps) { +/** A click on a value submits it. */ +export function BooleanFilter({ config, initialFilter, onSubmit }: BooleanFilterProps) { const { propertyKey, propertyLabel } = config + // Only the edit dialog has a current value. In the Add Filter menu the buttons are plain buttons, not toggles. + const isPressed = (value: boolean) => (initialFilter ? initialFilter.value === value : undefined) const labelId = useId() const handleSubmit = (selectedValue: boolean) => { onSubmit({ @@ -24,14 +26,16 @@ export function BooleanFilter({ config, onSubmit }: BooleanFilterProps) {
{propertyLabel}