diff --git a/web/src/shell/AppShell.test.tsx b/web/src/shell/AppShell.test.tsx index 539231c..a7ba038 100644 --- a/web/src/shell/AppShell.test.tsx +++ b/web/src/shell/AppShell.test.tsx @@ -2,7 +2,7 @@ import { render, screen, within } from '@testing-library/react' import { MemoryRouter } from 'react-router' import { afterEach, describe, expect, it } from 'vitest' import App from '../App.tsx' -import { declaredStyle } from '../test/cssRules.ts' +import { declaredStyle, outerSize } from '../test/cssRules.ts' import { expectMinHitArea } from '../test/hitArea.ts' import { mockViewport } from '../test/matchMedia.ts' import bottomBarStyles from './BottomBar.module.css' @@ -69,6 +69,7 @@ describe('AppShell', () => { expect(bar.right).toBe('12px') expect(bar.bottom).toBe('calc(16px + env(safe-area-inset-bottom))') expect(bar.height).toBe('62px') + expect(outerSize(nav(), 'height')).toBe(62) expect(bar.background).toBe('var(--color-text)') expect(bar.borderRadius).toBe('var(--radius-pill)') }) @@ -111,9 +112,12 @@ describe('AppShell', () => { expect(screen.getAllByRole('navigation')).toHaveLength(1) expect(nav()).toHaveClass(railStyles.rail!) expect(declaredStyle(nav()).width).toBe('96px') + expect(outerSize(nav(), 'width')).toBe(96) expect(declaredStyle(nav()).background).toBe('var(--color-surface)') const mark = within(nav()).getByText('M') expect(declaredStyle(mark).width).toBe('48px') + expect(outerSize(mark, 'width')).toBe(48) + expect(outerSize(mark, 'height')).toBe(48) expect(declaredStyle(mark).background).toBe('var(--color-accent)') expect(screen.queryByText('MealQuest')).not.toBeInTheDocument() expect(screen.queryByText('Our kitchen')).not.toBeInTheDocument() @@ -122,6 +126,8 @@ describe('AppShell', () => { const pill = link.firstElementChild! expect(declaredStyle(pill).width).toBe('56px') expect(declaredStyle(pill).height).toBe('36px') + expect(outerSize(pill, 'width')).toBe(56) + expect(outerSize(pill, 'height')).toBe(36) expect(declaredStyle(link).fontSize).toBe('11px') expect(declaredStyle(link).fontWeight).toBe('700') expectMinHitArea(link) @@ -138,6 +144,8 @@ describe('AppShell', () => { expect(declaredStyle(sidebar).width).toBe('208px') expect(declaredStyle(sidebar).background).toBe('var(--color-surface)') expect(declaredStyle(sidebar).padding).toBe('28px 16px') + // The 16 px side padding sits inside the 208 px, not outside it. + expect(outerSize(sidebar, 'width')).toBe(208) const wordmark = within(sidebar).getByText('MealQuest') expect(declaredStyle(wordmark).fontFamily).toBe('var(--font-heading)') diff --git a/web/src/shell/BottomBar.module.css b/web/src/shell/BottomBar.module.css index 92d8cb7..6d4e044 100644 --- a/web/src/shell/BottomBar.module.css +++ b/web/src/shell/BottomBar.module.css @@ -5,6 +5,7 @@ bottom: calc(16px + env(safe-area-inset-bottom)); height: 62px; padding: 0 6px; + box-sizing: border-box; display: flex; justify-content: space-around; align-items: center; diff --git a/web/src/shell/Rail.module.css b/web/src/shell/Rail.module.css index d84eefd..f9da188 100644 --- a/web/src/shell/Rail.module.css +++ b/web/src/shell/Rail.module.css @@ -1,6 +1,7 @@ .rail { width: 96px; padding: 24px 0; + box-sizing: border-box; display: flex; flex-direction: column; align-items: center; diff --git a/web/src/shell/Sidebar.module.css b/web/src/shell/Sidebar.module.css index 05f223c..4aa11ce 100644 --- a/web/src/shell/Sidebar.module.css +++ b/web/src/shell/Sidebar.module.css @@ -1,6 +1,7 @@ .sidebar { width: 208px; padding: 28px 16px; + box-sizing: border-box; display: flex; flex-direction: column; background: var(--color-surface); diff --git a/web/src/test/cssRules.ts b/web/src/test/cssRules.ts index e8413aa..3260596 100644 --- a/web/src/test/cssRules.ts +++ b/web/src/test/cssRules.ts @@ -48,3 +48,28 @@ export function animationKeyframes(element: Element): CSSKeyframesRule { if (!keyframes) throw new Error(`no @keyframes named ${JSON.stringify(name)}`) return keyframes } + +function px(value: string, what: string): number { + if (value === '') return 0 + const match = /^(\d+(?:\.\d+)?)px$/.exec(value) + if (!match) throw new Error(`${what} ${JSON.stringify(value)} is not a px length`) + return Number(match[1]) +} + +// The border-box size the element's declared rules give it on one axis: +// width or height, plus padding and border unless box-sizing is border-box. +// jsdom does no layout (getBoundingClientRect is all zeros), so this is the +// way to catch padding that would push a fixed box past its specified size. +export function outerSize(element: Element, axis: 'width' | 'height'): number { + const style = declaredStyle(element) + const size = px(style.getPropertyValue(axis), axis) + if (style.boxSizing === 'border-box') return size + const sides = axis === 'width' ? ['left', 'right'] : ['top', 'bottom'] + return sides.reduce( + (total, side) => + total + + px(style.getPropertyValue(`padding-${side}`), `padding-${side}`) + + px(style.getPropertyValue(`border-${side}-width`), `border-${side}-width`), + size, + ) +}