, "ref">) {
return (
) {
= {
+const pageNameByRoutePath: Partial> = {
'/': 'Home',
'/about': 'About',
'/components': 'Components',
@@ -44,9 +45,9 @@ const routeNames: Record = {
function RootComponent() {
const router = useRouter()
const routerState = useRouterState()
- const currentPath = routerState.location.pathname
+ const routePath = routerState.matches[routerState.matches.length - 1]?.fullPath ?? ''
const isNotFound = routerState.matches.some((match) => match.globalNotFound)
- const pageName = isNotFound ? 'Not Found' : (routeNames[currentPath] ?? 'Page')
+ const pageName = isNotFound ? 'Not Found' : (pageNameByRoutePath[routePath] ?? 'Page')
return (
@@ -76,7 +77,8 @@ function RootComponent() {
-
+ {/* The default container is a footer, which is a contentinfo landmark. */}
+
)
diff --git a/frontend/tests/features/app-shell/app-shell.locators.ts b/frontend/tests/features/app-shell/app-shell.locators.ts
index 77391500..f323fa08 100644
--- a/frontend/tests/features/app-shell/app-shell.locators.ts
+++ b/frontend/tests/features/app-shell/app-shell.locators.ts
@@ -4,7 +4,14 @@ 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'),
+ contentInfoLandmarks: page.getByRole('contentinfo'),
+ // The About page has no separator of its own, so the one separator in main is the one of the layout header.
+ headerSeparator: page.getByRole('main').getByRole('separator'),
+ routerDevtoolsButton: page.getByRole('button', { name: 'Open TanStack Router Devtools' }),
+ mainNavigation: page.getByRole('navigation', { name: 'Main' }),
+ sidebarTrigger: page.getByRole('main').getByRole('button', { name: 'Toggle Sidebar' }),
mainLandmarks: page.getByRole('main'),
+ wideContent: page.getByRole('region', { name: 'Wide content' }),
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 5959056b..bcf41ea4 100644
--- a/frontend/tests/features/app-shell/app-shell.spec.ts
+++ b/frontend/tests/features/app-shell/app-shell.spec.ts
@@ -52,6 +52,52 @@ test.describe('the landmarks of a page', () => {
})
})
+test.describe('the layout header', () => {
+ test.use({ viewport: { width: 1280, height: 720 } })
+
+ test('centers the separator on the sidebar trigger', async ({ page }) => {
+ const shell = appShellPage(page)
+
+ await shell.openAbout()
+
+ expect(await shell.verticalCenter(shell.at.headerSeparator)).toBeCloseTo(await shell.verticalCenter(shell.at.sidebarTrigger), 0)
+ })
+})
+
+test.describe('the landmarks of the layout', () => {
+ test('the router devtools add no contentinfo landmark', async ({ page }) => {
+ const shell = appShellPage(page)
+
+ await shell.openAbout()
+
+ await expect(shell.at.routerDevtoolsButton).toBeVisible()
+ expect(await shell.at.contentInfoLandmarks.count()).toBe(0)
+ })
+
+ test('the sidebar links are in a main navigation landmark', async ({ page }) => {
+ const shell = appShellPage(page)
+
+ await shell.openAbout()
+
+ await expect(shell.at.mainNavigation.getByRole('link', { name: 'Home' })).toBeVisible()
+ await expect(shell.at.mainNavigation.getByRole('link', { name: 'About' })).toBeVisible()
+ })
+})
+
+test.describe('wide content next to the open sidebar', () => {
+ test.use({ viewport: { width: 1024, height: 720 } })
+
+ test('scrolls in its own box, and the page does not scroll sideways', async ({ page }) => {
+ const shell = appShellPage(page)
+ await shell.openAbout()
+
+ await shell.addWideContent(1600)
+
+ expect(await shell.sideScrollRange(shell.at.wideContent)).toBeGreaterThan(0)
+ expect(await shell.pageSideScrollRange()).toBe(0)
+ })
+})
+
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/app-shell/app-shell.tasks.ts b/frontend/tests/features/app-shell/app-shell.tasks.ts
index ea6572d0..790f8483 100644
--- a/frontend/tests/features/app-shell/app-shell.tasks.ts
+++ b/frontend/tests/features/app-shell/app-shell.tasks.ts
@@ -1,4 +1,4 @@
-import { expect, type Page } from '@playwright/test'
+import { expect, type Locator, type Page } from '@playwright/test'
import { weatherPaths } from '@tests/support/api/weather-api'
import { appShell } from './app-shell.locators'
@@ -31,6 +31,34 @@ export function appShellPage(page: Page) {
await settled
},
+ async addWideContent(width: number) {
+ await at.mainLandmarks.evaluate((main, width) => {
+ const scroller = document.createElement('div')
+ scroller.className = 'overflow-x-auto'
+ scroller.role = 'region'
+ scroller.ariaLabel = 'Wide content'
+ const content = document.createElement('div')
+ content.style.width = `${width}px`
+ content.textContent = 'Wide content'
+ scroller.append(content)
+ main.append(scroller)
+ }, width)
+ },
+
+ async pageSideScrollRange() {
+ return page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)
+ },
+
+ async sideScrollRange(locator: Locator) {
+ return locator.evaluate((element) => element.scrollWidth - element.clientWidth)
+ },
+
+ async verticalCenter(locator: Locator) {
+ const box = await locator.boundingBox()
+ if (!box) throw new Error('The element has no box')
+ return box.y + box.height / 2
+ },
+
async pageScrollRange() {
return page.evaluate(() => document.documentElement.scrollHeight - document.documentElement.clientHeight)
},
diff --git a/frontend/tests/features/filter-demo/filter-demo.tasks.ts b/frontend/tests/features/filter-demo/filter-demo.tasks.ts
index 24923abe..b14f0fee 100644
--- a/frontend/tests/features/filter-demo/filter-demo.tasks.ts
+++ b/frontend/tests/features/filter-demo/filter-demo.tasks.ts
@@ -190,13 +190,13 @@ export function filterDemoPage(page: Page) {
const { width, height } = element.getBoundingClientRect()
return width > 1 && height > 1
}
- const headerBottom = title.parentElement!.getBoundingClientRect().bottom
+ const titleBottom = title.getBoundingClientRect().bottom
let top = Infinity
let bottom = -Infinity
for (const element of sheet!.querySelectorAll('*')) {
const box = element.getBoundingClientRect()
const isLeaf = ['BUTTON', 'INPUT'].includes(element.tagName) || ![...element.children].some(shows)
- if (!isLeaf || !shows(element) || box.top < headerBottom) continue
+ if (!isLeaf || !shows(element) || box.top < titleBottom) continue
top = Math.min(top, box.top)
bottom = Math.max(bottom, box.bottom)
}
@@ -205,7 +205,7 @@ export function filterDemoPage(page: Page) {
scroller = scroller.parentElement
}
const contentEnd = scroller.getBoundingClientRect().top + scroller.clientTop - scroller.scrollTop + scroller.scrollHeight
- return { top: Math.round(top - headerBottom), bottom: Math.round(contentEnd - bottom) }
+ return { top: Math.round(top - titleBottom), bottom: Math.round(contentEnd - bottom) }
}, sheet)
},