|
1 | 1 | import { test, expect, Page } from '@playwright/test'; |
2 | | -import { collectPageErrors, assertNoPageErrors, assertNoHorizontalOverflow } from './utils'; |
| 2 | +import { collectPageErrors, assertNoPageErrors, assertNoHorizontalOverflow, pageOverflow } from './utils'; |
3 | 3 |
|
4 | 4 | /* |
5 | 5 | * Book-detail completeness pass: |
@@ -182,3 +182,59 @@ test('book detail with a "More by" strip has no horizontal overflow on mobile', |
182 | 182 | await expect(page.locator('section[aria-label^="More by"]')).toBeVisible({ timeout: 10_000 }); |
183 | 183 | await assertNoHorizontalOverflow(page); |
184 | 184 | }); |
| 185 | + |
| 186 | +// A reader without the edit role sees the read-only tag pills, and those pills |
| 187 | +// were `white-space: nowrap` with no max-width. Real libraries carry |
| 188 | +// Library-of-Congress subject headings ("France -- History -- Revolution, |
| 189 | +// 1789-1799 -- Fiction") that are wider than a phone, so one tag dragged the |
| 190 | +// whole detail page into horizontal scroll. The admin-role branch renders |
| 191 | +// removable chips instead and wraps fine, which is why the existing mobile |
| 192 | +// specs — all of which log in as admin — never saw this. |
| 193 | +// |
| 194 | +// Deterministic on any seed: the role and the tag list are both stubbed. |
| 195 | +// |
| 196 | +// Measures the DELTA the long tags add, not absolute overflow. The CI fixture's |
| 197 | +// detail page carries ~35px of horizontal overflow from an unrelated cause |
| 198 | +// (notes/e2e-mobile-overflow-ci-triage.md), and an absolute assertion here would |
| 199 | +// fail on that instead of on the tag row, reporting nothing about the pills. The |
| 200 | +// delta is what this fix owns: pre-fix it was ~300px, post-fix it is 0. |
| 201 | +test('long tag names add no horizontal overflow to the read-only detail page', async ({ page }) => { |
| 202 | + await page.setViewportSize({ width: 390, height: 844 }); |
| 203 | + await page.goto('/app'); |
| 204 | + const bookId = await firstBookId(page); |
| 205 | + test.skip(bookId == null, 'seed has no books'); |
| 206 | + |
| 207 | + // Drop the edit role so the page renders the read-only Pill branch that a |
| 208 | + // guest or viewer account gets. |
| 209 | + await page.route('**/api/v1/auth/me', async (route) => { |
| 210 | + const res = await route.fetch(); |
| 211 | + const me = await res.json(); |
| 212 | + if (me?.role) me.role.edit = false; |
| 213 | + await route.fulfill({ response: res, json: me }); |
| 214 | + }); |
| 215 | + |
| 216 | + await page.goto(`/app/book/${bookId}`, { waitUntil: 'domcontentloaded' }); |
| 217 | + await expect(page.locator('main h1')).toBeVisible({ timeout: 10_000 }); |
| 218 | + const baseline = await pageOverflow(page); |
| 219 | + |
| 220 | + // A real LoC heading plus a single unbroken token wider than the viewport. |
| 221 | + const longTag = 'France -- History -- Revolution, 1789-1799 -- Fiction'; |
| 222 | + await page.route(`**/api/v1/books/${bookId}`, async (route) => { |
| 223 | + const res = await route.fetch(); |
| 224 | + const book = await res.json(); |
| 225 | + book.tags = [ |
| 226 | + { id: 990001, name: longTag }, |
| 227 | + { id: 990002, name: 'Bildungsroman'.repeat(8) }, |
| 228 | + ]; |
| 229 | + await route.fulfill({ response: res, json: book }); |
| 230 | + }); |
| 231 | + |
| 232 | + await page.reload({ waitUntil: 'domcontentloaded' }); |
| 233 | + await expect(page.getByText(longTag)).toBeVisible({ timeout: 10_000 }); |
| 234 | + const withLongTags = await pageOverflow(page); |
| 235 | + |
| 236 | + expect( |
| 237 | + withLongTags - baseline, |
| 238 | + `long tags widened the page by ${withLongTags - baseline}px (baseline ${baseline}px)`, |
| 239 | + ).toBeLessThanOrEqual(1); |
| 240 | +}); |
0 commit comments