Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions frontend/e2e/header.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { expect, test, type Page } from '@playwright/test'
import { mockGraphql } from './mock-graphql'

function header(page: Page) {
const banner = page.getByRole('banner')
return {
home: banner.getByRole('link', { name: 'UFOund it' }),
report: banner.getByRole('link', { name: 'Report a sighting' }),
theme: banner.getByRole('switch', { name: 'Dark mode' }),
}
}

async function boxOf(locator: ReturnType<Page['locator']>) {
const box = await locator.boundingBox()
if (!box) throw new Error('Not rendered')
return box
}

// The layout comes from CSS media queries, so it is checked in a real
// browser at the widths where it changes.
for (const colorScheme of ['light', 'dark'] as const) {
test(`puts the report link and theme switch on one row at 320 px (${colorScheme})`, async ({
page,
}) => {
await page.setViewportSize({ width: 320, height: 640 })
await page.emulateMedia({ colorScheme })
await mockGraphql(page)
await page.goto('./')
const { home, report, theme } = header(page)

const homeBox = await boxOf(home)
const reportBox = await boxOf(report)
const themeBox = await boxOf(theme)

// The logo has the first row, the link and the switch share the second
// at the same height, and the rows line up at the left edge.
expect(reportBox.y).toBeGreaterThan(homeBox.y + homeBox.height)
expect(reportBox.y).toBe(themeBox.y)
expect(reportBox.height).toBe(themeBox.height)
expect(reportBox.x).toBe(homeBox.x)
expect(reportBox.x + reportBox.width).toBeLessThan(themeBox.x)
expect(themeBox.x + themeBox.width).toBeLessThanOrEqual(320)
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBe(320)

// The Tab order follows the rows.
await home.focus()
await page.keyboard.press('Tab')
await expect(report).toBeFocused()
await page.keyboard.press('Tab')
await expect(theme).toBeFocused()
})
}

// Just above the two-row layout, the smaller logo keeps the header from
// wrapping on its own.
for (const width of [481, 520]) {
test(`keeps the header on one row at ${width} px`, async ({ page }) => {
await page.setViewportSize({ width, height: 640 })
await mockGraphql(page)
await page.goto('./')
const { home, report, theme } = header(page)

const homeBox = await boxOf(home)
for (const box of [await boxOf(report), await boxOf(theme)]) {
expect(box.y).toBeLessThan(homeBox.y + homeBox.height)
}
})
}
37 changes: 36 additions & 1 deletion frontend/src/components/Banner.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@
color: var(--accent);
}

@media (max-width: 480px) {
/* The full-size logo, the link and the switch need about 490 px, so the
smaller logo starts above the 480 px where the header wraps. Otherwise
it would wrap on its own, with the switch between the two rows. */
@media (max-width: 520px) {
.banner__title {
gap: 8px;
font-size: 1.3rem;
Expand All @@ -108,6 +111,38 @@
}
}

/* Too narrow for one row, so the logo gets the first row, and the link
and the switch share the second, in the same order as the Tab order.
The link fills the space next to the switch, so both rows line up with
the page's edges, and is as tall as the switch. */
@media (max-width: 480px) {
.banner__content {
flex-wrap: nowrap;
align-items: flex-end;
gap: 12px;
}

.banner__nav {
flex-direction: column;
align-items: flex-start;
gap: 12px;
min-width: 0;
}

.banner__submit {
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
min-height: 42px;
box-sizing: border-box;
}

.banner__theme {
flex: 0 0 auto;
}
}

/* A pill with a moon on the left and a sun on the right. The thumb slides
behind the icon of the mode in use. */
.banner__theme {
Expand Down
Loading