diff --git a/app/fonts/instrument-serif-latin-400.woff2 b/app/fonts/instrument-serif-latin-400.woff2 new file mode 100644 index 0000000..0ad6971 Binary files /dev/null and b/app/fonts/instrument-serif-latin-400.woff2 differ diff --git a/app/globals.css b/app/globals.css index cb80858..fff215d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,6 +1,6 @@ @import "tailwindcss"; -/* Manrope is bundled by Vite from app/fonts. Instrument Serif is embedded below as a data URI (single 21 KB Latin subset, regular only) so the repo carries no extra font file. */ +/* Fonts are bundled by Vite from app/fonts and served under the same origin CSP. */ @font-face { font-family: "Manrope"; src: url("./fonts/manrope-latin-variable.woff2") format("woff2"); @@ -12,7 +12,7 @@ /* Instrument Serif (SIL OFL 1.1), Latin subset, regular only. */ @font-face { font-family: "Instrument Serif"; - src: url("data:font/woff2;base64,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") format("woff2"); + src: url("./fonts/instrument-serif-latin-400.woff2") format("woff2"); font-style: normal; font-weight: 400; font-display: swap; diff --git a/components/concepts/signal-desk/pond-input.ts b/components/concepts/signal-desk/pond-input.ts index 8ba6282..d4ffd2f 100644 --- a/components/concepts/signal-desk/pond-input.ts +++ b/components/concepts/signal-desk/pond-input.ts @@ -101,19 +101,6 @@ export function installPondInput(options: PondInputOptions) { if (activeTouch?.id === event.pointerId) activeTouch = null; }; - const feedFromControl = (event: Event) => { - for (const y of [0.5, 0.3, 0.7, 0.15, 0.85, 0.05, 0.95]) { - for (const x of [0.5, 0.3, 0.7, 0.15, 0.85, 0.05, 0.95]) { - const screen = { x: innerWidth * x, y: innerHeight * y }; - const obscured = [...document.querySelectorAll("[data-pond-obscures]")].some(element => { - const box = element.getBoundingClientRect(); - return screen.x >= box.left && screen.x <= box.right && screen.y >= box.top && screen.y <= box.bottom; - }); - if (!obscured && ownsPoint(screen) && dropFood(screen)) { event.preventDefault(); return; } - } - } - }; - const resize = () => { activeTouch = null; }; @@ -122,7 +109,6 @@ export function installPondInput(options: PondInputOptions) { interaction.addEventListener("pointerdown", press, { passive: true }); interaction.addEventListener("pointerup", release, { passive: false }); interaction.addEventListener("pointercancel", cancel, { passive: true }); - interaction.addEventListener("pond-feed", feedFromControl); window.addEventListener("resize", resize, { passive: true }); return () => { @@ -130,7 +116,6 @@ export function installPondInput(options: PondInputOptions) { interaction.removeEventListener("pointerdown", press); interaction.removeEventListener("pointerup", release); interaction.removeEventListener("pointercancel", cancel); - interaction.removeEventListener("pond-feed", feedFromControl); window.removeEventListener("resize", resize); }; } diff --git a/components/concepts/signal-desk/signal-desk.module.css b/components/concepts/signal-desk/signal-desk.module.css index 5f6b3f5..fc8ea90 100644 --- a/components/concepts/signal-desk/signal-desk.module.css +++ b/components/concepts/signal-desk/signal-desk.module.css @@ -350,28 +350,6 @@ border-radius:50%; } -.feedButton { - display:inline-flex; - align-items:center; - gap:6px; - min-width:44px; - min-height:44px; - margin:0 0 0 2px; - padding:0 12px; - border:1px solid var(--line-strong); - border-radius:2px; - background:var(--paper); - color:var(--ink); - font-size:14px; - font-weight:650; - cursor:pointer; -} - -.feedButton:disabled { - opacity:.6; - cursor:wait; -} - .pondNote { position:absolute; right:20px; @@ -751,9 +729,6 @@ .pondChoice span { padding:0 8px; } - .feedButton { - padding:0 9px; - } } @media(forced-colors:active) { diff --git a/components/concepts/signal-desk/signal-desk.tsx b/components/concepts/signal-desk/signal-desk.tsx index 0addf20..d2637c7 100644 --- a/components/concepts/signal-desk/signal-desk.tsx +++ b/components/concepts/signal-desk/signal-desk.tsx @@ -215,7 +215,6 @@ export function SignalDesk() { return () => { window.removeEventListener("scroll", sync); window.removeEventListener("hashchange", sync); }; }, []); const [pondStatus, setPondStatus] = useState<{ mode: string; renderer: string }>({ mode: "ascii", renderer: "loading" }); - const [feedStatus, setFeedStatus] = useState(""); const entrance = ready && !reduced; useEffect(() => { @@ -231,7 +230,7 @@ export function SignalDesk() { return (
-

The decorative pond responds to pointer movement. Click or tap open water to feed the fish, or use the Feed fish button in the pond controls.

+

The decorative pond responds to pointer movement. Click or tap open water to feed the fish.

@@ -358,11 +357,6 @@ export function SignalDesk() { ))}
-
{pondStatus.mode !== pondView || pondStatus.renderer === "loading" ? `Loading ${pondView === "ascii" ? "ASCII" : "Pixel"} view…` @@ -370,7 +364,6 @@ export function SignalDesk() { ? "Pond unavailable. Try the other view." : `Current view: ${pondView === "ascii" ? "ASCII" : "Pixel"}`}
- {feedStatus} diff --git a/docs/verification/design-92-browser-drift.md b/docs/verification/design-92-browser-drift.md new file mode 100644 index 0000000..cd11d86 --- /dev/null +++ b/docs/verification/design-92-browser-drift.md @@ -0,0 +1,71 @@ +# Browser verification after design PR 92 + +Base: `d3bb71254ae1e6de4c0a0c248827cbee5812b284`. + +The original [verification run](https://github.com/luinbytes/luinbytes.dev/actions/runs/42925766714) +ran 48 tests and reported 18 failures and 8 errors, including subtests. +The diagnosis compares those failures with [PR 92's stated intent](https://github.com/luinbytes/luinbytes.dev/pull/92) +and the merged source. No changes from PR 93 are included. + +| Original failure | First divergence | Resolution and retained coverage | +| --- | --- | --- | +| `test_copyright_year_hydrates_when_the_visitors_clock_differs` | Instrument Serif's data URL violates `font-src 'self'` | Actual regression. Extract the identical 21,032 font bytes into `app/fonts`, bundled by Vite. Keep CSP and console assertions unchanged; add explicit font loading coverage. | +| `test_homepage_is_responsive_and_routes_featured_work` at desktop and mobile | Project buttons no longer exist | Inspect all four cards, headline figure, project actions and event isolation. | +| `test_project_marker_is_centered_for_every_selection` at 1440, 390 and 320 | Removed selection buttons and marker | Replace with all four cards visible without selection, readable headings and visible project actions at all three sizes. Wrapping is allowed; horizontal text overflow is not. | +| `test_homepage_preserves_layout_and_world_state_across_viewport_matrix` at nine sizes | Removed tab selectors yield zero targets | Check card action target sizes, one column or two column grid geometry and heading containment. Preserve fish continuity, separation, water bounds, pond size and overflow checks. | +| `test_mobile_navigation_hero_viewport_and_system_color_modes` at 320 and 390 | About is now More | Match More to the existing `#about` anchor; retain focus, touch target, visibility, contrast and theme assertions. | +| `test_navigation_and_bordered_controls_keep_their_visual_hierarchy` | About label, then tabbed project assumptions | Match More and inspect card labels, project links, illustrations and proof tags directly. Scope illustration selectors to artwork, since actions now precede the illustration and their 15px link icons are the first SVGs. | +| `test_actions_captions_privacy_and_immediate_navigation` | About locator never matches | Use More; retain immediate active section, privacy, caption and layout checks. | +| `test_pointer_stirs_the_pond` | Page selection is now auto | Expect restored page selection; preserve pointer reaction, ripple, wake, water safety and clean console assertions. | +| `test_hero_copy_does_not_block_pointer_water_reaction` | Hero paper card intentionally owns pointer input and is marked `data-pond-obscures` | Check selectable copy blocks food input and affordance. Open water still has dedicated pointer coverage. | +| `test_pixel_food_pause_and_reduced_motion` | Right click no longer feeds | Use single primary click; retain Pixel food, reduced motion, pause and resume checks. | +| `test_food_drop_reservation_and_feeding` | Right click no longer feeds or suppresses context menu | Assert context menu is allowed and does not feed, then primary click feeds at requested coordinates. Retain reservation, completed feeding, capacity and water safety checks. | +| `test_reduced_motion_preserves_the_ecosystem_at_low_speed` | Right click no longer feeds | Use primary click; retain fish motion, water motion, live renderer and clean console checks. | +| `test_mobile_touch_disturbs_the_shared_pond` | Single tap now reports tap-food and drops food | Require one pellet and one splash from one real touch tap. Retain coordinate agreement, UI isolation and rejection of moved, cancelled and multiple touch gestures. | +| `test_every_scoped_route_exports_a_production_large_image_card` | Share card alt intentionally changed | Match descriptive alt. Preserve PNG dimensions, hash and metadata consistency checks. | + +## Scope and limits + +No design, interaction implementation, security policy, workflow or feeding access +changes. Font bytes are identical to the embedded source; existing OFL license +remains in `app/fonts`. Verification uses the production export on loopback, +not a deployment. Mac Chromium evidence does not establish Linux Actions or +physical mobile device results. + +## Verification on 11 November 2026 + +Confirmed font cause locally before the fix: production build followed by +`python -m unittest discover -s tests/browser -p '*_test.py' -k copyright -v` +failed with the same CSP console error as Actions. After the fix that test and +the explicit font loading test both pass. + +Final `npm run test:e2e` passed all 49 tests in 214.724 seconds, with no skips. +This includes a fresh production build. All original failing cases, including +the replaced project selection case, are covered in the full rerun. The first +updated suite exposed one further stale selector: the new project link icon +preceded the HomeBot artwork. Scoping the selector to `projectIllustration` +restored the original meaningful width assertion; the final suite passes it. + +`npm run lint`, `npm run test:pond` (54 tests), `npm audit --audit-level=high`, +and `git diff --check` also pass. Audit reports seven existing moderate +vulnerabilities. Dependencies are unchanged. Environment: macOS, Node 26.5.0, +Python 3.14.7, Playwright 1.61.0 and Chromium 1228. Actions uses Node 22 on Linux; +that environment was not run locally. + +The extracted font SHA256 is +`5eb09b5ac0e28b67c2f041c8ba6d244604ca0c0980d65912ab2d47fed84ddc31`, +matching the decoded data URI on the base commit. + +Local evidence is retained beside the isolated checkout in +`design-baseline-local.log`, `design-e2e-first.log`, `design-e2e-final.log`, +`design-lint-final.log`, `design-pond.log`, and `design-audit.log`. +Generated `test-results/project-grid-{320,390,1440}.png` and +`test-results/portfolio-final-{desktop,mobile}.png` show the restored font +and grid. Reviewed those screenshots for heading readability and horizontal +containment. Full page captures include fixed element and offscreen animation +artifacts and do not replace the viewport assertions. + +Change Review: no findings in the final diff. No tests disabled, security +assertions loosened, or shared interaction code changed. Temporary Python +environment removed after verification. Local dependencies, production export, +and evidence files remain for review; generated sitemap date changes are excluded. diff --git a/tests/browser/browser_test.py b/tests/browser/browser_test.py index fb392b1..74d82d8 100644 --- a/tests/browser/browser_test.py +++ b/tests/browser/browser_test.py @@ -43,7 +43,7 @@ ) PORTFOLIO_NAV_LINKS = ( ("Work", "#work"), - ("About", "#about"), + ("More", "#about"), ("Contact", "#contact"), ) @@ -291,7 +291,7 @@ class ShareCardStaticExportTests(unittest.TestCase): "/share-cards/luinbytes-dev-pond.png": "f2f09d3711b27c857c74eb34d8420354dfec2635e30153fe884d3095d24b3583", } route_alts = { - "/": "Lu | Software Engineer", + "/": "luinbytes.dev share card: the line “I get annoyed, then I build the missing thing.” beside a pixel-art pond with koi, lily pads and stepping stones.", } def test_editable_source_contains_the_original_card_copy(self) -> None: @@ -767,7 +767,7 @@ def test_pixel_food_pause_and_reduced_motion(self) -> None: wait_for_condition(page, "before => document.querySelector('[data-pond-state]')?.dataset.fishPositions !== before", arg=positions) x, y = find_open_water(page, pond) before = int(pond.get_attribute("data-food-dropped-count")) - page.mouse.click(x, y, button="right") + page.mouse.click(x, y) wait_for_condition(page, "before => Number(document.querySelector('[data-pond-state]')?.dataset.foodDroppedCount) > before", arg=before) self.assertGreater(int(pond.get_attribute("data-food-count")), 0) page.evaluate("""() => { @@ -1029,6 +1029,21 @@ def test_copyright_year_hydrates_when_the_visitors_clock_differs(self) -> None: expect(page.locator("footer")).to_contain_text(expected) self.assertEqual(issues, []) + def test_display_font_loads_under_the_production_security_policy(self) -> None: + browser = self.playwright.chromium.launch() + self.addCleanup(browser.close) + page = browser.new_page() + issues = [] + page.on("console", lambda message: record_console_issue(issues, message)) + page.goto(self.base_url, wait_until="networkidle") + loaded = page.evaluate("""async () => { + const faces = await document.fonts.load('46px "Instrument Serif"', 'Lu'); + return faces.length === 1 && faces[0].status === 'loaded'; + }""") + self.assertTrue(loaded) + self.assertIn('Instrument Serif', page.locator('#hero-title').evaluate('element => getComputedStyle(element).fontFamily')) + self.assertEqual(issues, []) + def test_homepage_is_responsive_and_routes_featured_work(self) -> None: for viewport_name, viewport in ( ("desktop", {"width": 1440, "height": 900}), @@ -1046,34 +1061,19 @@ def test_homepage_is_responsive_and_routes_featured_work(self) -> None: ).is_visible() ) expect(page.locator("#hero-title")).to_have_css("clip-path", "none") + cards = page.locator("#work article") + self.assertEqual(cards.count(), 4) for project in ("Rakazo", "linux-sonar", "HomeBot", "ConnectorMap"): - self.assertTrue( - page.get_by_role("button", name=re.compile(project, re.I)).is_visible() - ) - - wait_for_condition( - page, - "document.querySelector('[data-pond-state]')?.dataset.pondState === 'running'" - ) - impact_count = page.locator("[data-pond-state]").get_attribute( - "data-primary-impact-count" - ) - self.assertTrue( - page.get_by_role("group", name="Featured projects").is_visible() - ) - page.get_by_role("button", name=re.compile("Rakazo", re.I)).click() - self.assertEqual( - page.locator("[data-pond-state]").get_attribute( - "data-primary-impact-count" - ), - impact_count, - ) - proof = page.get_by_text("4 highlighted merged PRs", exact=True) - proof.wait_for(state="visible") - self.assertTrue(proof.is_visible()) - self.assertTrue( - page.get_by_role("link", name="Merged work").is_visible() - ) + expect(cards.get_by_role("heading", name=project, exact=True)).to_be_visible() + wait_for_condition(page, "document.querySelector('[data-pond-state]')?.dataset.pondState === 'running'") + impact_count = page.locator("[data-pond-state]").get_attribute("data-primary-impact-count") + heading = cards.first.get_by_role("heading") + heading.evaluate("element => element.scrollIntoView({ block: 'center', behavior: 'instant' })") + heading.click() + self.assertEqual(page.locator("[data-pond-state]").get_attribute("data-primary-impact-count"), impact_count) + expect(cards.first.locator("[class*=projectFigure] strong")).to_have_text("4") + expect(cards.first.get_by_text("highlighted merged PRs upstream", exact=True)).to_be_visible() + expect(cards.first.get_by_role("link", name="Merged work")).to_be_visible() self.assertLessEqual( page.evaluate("document.documentElement.scrollWidth"), viewport["width"], @@ -1087,46 +1087,28 @@ def test_homepage_is_responsive_and_routes_featured_work(self) -> None: ) browser.close() - def test_project_marker_is_centered_for_every_selection(self) -> None: + def test_project_grid_shows_every_card_without_selection(self) -> None: browser = self.playwright.chromium.launch() self.addCleanup(browser.close) for width, height in ((1440, 900), (390, 844), (320, 800)): with self.subTest(width=width): page = browser.new_page(viewport={"width": width, "height": height}) page.goto(self.base_url, wait_until="networkidle") - buttons = page.get_by_role("group", name="Featured projects").get_by_role("button") - for index in range(4): - button = buttons.nth(index) - button.click() - page.wait_for_timeout(400) - self.assertEqual(button.get_attribute("aria-pressed"), "true") - centers = button.evaluate("""async button => { - const marker = button.querySelector('i'); - let previous = null; - let stable = 0; - const deadline = performance.now() + 5000; - while (performance.now() < deadline) { - await new Promise(requestAnimationFrame); - const bounds = button.getBoundingClientRect(); - const dot = marker.getBoundingClientRect(); - const centers = [bounds.y + bounds.height / 2, dot.y + dot.height / 2]; - stable = previous && centers.every((value, index) => Math.abs(value - previous[index]) < 0.05) ? stable + 1 : 0; - if (stable >= 3) return centers; - previous = centers; - } - throw new Error('Project marker did not settle'); - }""") - self.assertAlmostEqual(centers[0], centers[1], delta=0.75) - if width <= 390: - heading = page.locator("#work article h3") - expect(heading).to_have_text(button.locator("strong").inner_text()) - heading.scroll_into_view_if_needed() - self.assertTrue(heading.evaluate("""element => { - const range = document.createRange(); - range.selectNodeContents(element); - const lines = [...range.getClientRects()]; - return lines.length === 1 && lines[0].width <= element.clientWidth; - }"""), f"Project heading wraps at {width}px") + cards = page.locator("#work article") + self.assertEqual(cards.count(), 4) + self.assertEqual(page.locator("#work [aria-pressed]").count(), 0) + for index, project in enumerate(("Rakazo", "linux-sonar", "HomeBot", "ConnectorMap")): + card = cards.nth(index) + heading = card.get_by_role("heading", name=project, exact=True) + card.scroll_into_view_if_needed() + expect(heading).to_be_visible() + expect(card.get_by_role("link", name=f"View project {project}", exact=True)).to_be_visible() + self.assertTrue(heading.evaluate("""element => { + const range = document.createRange(); + range.selectNodeContents(element); + return [...range.getClientRects()].every(line => line.width <= element.clientWidth + 1); + }"""), f"Project heading overflows at {width}px") + page.screenshot(path=SCREENSHOTS / f"project-grid-{width}.png", full_page=True) page.close() def test_mobile_navigation_hero_viewport_and_system_color_modes(self) -> None: @@ -1268,9 +1250,9 @@ def test_homepage_preserves_layout_and_world_state_across_viewport_matrix(self) const heading = document.querySelector('#hero-title').getBoundingClientRect(); const header = document.querySelector('header').getBoundingClientRect(); const pond = document.querySelector('[data-renderer]').getBoundingClientRect(); - const buttons = [...document.querySelectorAll('[aria-label="Featured projects"] button')] + const buttons = [...document.querySelectorAll('#work article [class*="projectActions"] a:first-child')] .map(element => element.getBoundingClientRect()); - const projectNames = [...document.querySelectorAll('[aria-label="Featured projects"] strong')]; + const projectNames = [...document.querySelectorAll('#work article h3')]; const aboutCards = [...document.querySelectorAll('#about > article, #about > div:last-child')]; const layoutWidth = document.documentElement.clientWidth; const fish = document.querySelector('[data-pond-state]').dataset.fishPositions @@ -1281,11 +1263,16 @@ def test_homepage_preserves_layout_and_world_state_across_viewport_matrix(self) headerInside: header.left >= -1 && header.right <= width + 1, pondFits: Math.abs(pond.width - width) < 1 && Math.abs(pond.height - height) < 1, projectTargets: buttons.length === 4 && buttons.every(button => button.width >= 44 && button.height >= 44), - projectDistribution: width <= 430 || (buttons.length === 4 && Math.abs(buttons[3].right - document.querySelector('[aria-label="Featured projects"]').getBoundingClientRect().right) < 2), + projectDistribution: (() => { + const cards = [...document.querySelectorAll('#work article')].map(card => card.getBoundingClientRect()); + return cards.length === 4 && (width <= 900 + ? cards.every((card, index) => index === 0 || card.top >= cards[index - 1].bottom) + : Math.abs(cards[0].top - cards[1].top) < 1 && cards[1].left >= cards[0].right && cards[2].top >= cards[0].bottom); + })(), projectNamesFit: width > 430 || projectNames.every(element => { const range = document.createRange(); range.selectNodeContents(element); - return range.getBoundingClientRect().width <= element.getBoundingClientRect().width + 1; + return [...range.getClientRects()].every(line => line.width <= element.getBoundingClientRect().width + 1); }), aboutCardsCentered: width > 430 || aboutCards.every(element => { const bounds = element.getBoundingClientRect(); @@ -1432,7 +1419,7 @@ def test_loading_shows_skeleton_then_fades_in_the_live_ecosystem(self) -> None: self.assertEqual(host.evaluate("element => getComputedStyle(element).opacity"), "1") browser.close() - def test_hero_copy_does_not_block_pointer_water_reaction(self) -> None: + def test_hero_copy_is_selectable_and_blocks_pond_input(self) -> None: browser = self.playwright.chromium.launch() page = self.pond_page(browser, viewport={"width": 1280, "height": 800}) page.goto(f"{self.base_url}/?pond-seed=e2e-hero-pointer", wait_until="networkidle") @@ -1453,7 +1440,12 @@ def test_hero_copy_does_not_block_pointer_water_reaction(self) -> None: box["y"] + box["height"] * vertical, ) responsive |= pond.get_attribute("data-food-affordance") == "true" - self.assertTrue(responsive) + self.assertFalse(responsive) + self.assertEqual(page.locator('#hero-title').evaluate('element => getComputedStyle(element).userSelect'), 'auto') + before_food = pond.get_attribute("data-food-dropped-count") + page.get_by_role("heading", name="I make stubborn software behave.").click() + page.wait_for_timeout(200) + self.assertEqual(pond.get_attribute("data-food-dropped-count"), before_food) action = page.get_by_role("link", name="Things I’ve made").bounding_box() page.mouse.move( @@ -1497,12 +1489,12 @@ def test_navigation_and_bordered_controls_keep_their_visual_hierarchy(self) -> N navigation.evaluate_all( "elements => elements.map(element => [element.textContent.trim(), element.getAttribute('href')])" ), - [["Work", "#work"], ["About", "#about"], ["Contact", "#contact"]], + [["Work", "#work"], ["More", "#about"], ["Contact", "#contact"]], ) expect(hero.get_by_role("img", name="Lu's illustrated avatar wearing a pink cap")).to_be_visible() expect(hero.get_by_role("heading", level=1)).to_have_text("Hi, I’m Lu.") expect(page.locator("header a[href='#top']")).to_be_visible() - category_labels = page.locator("[aria-label='Featured projects'] button small") + category_labels = page.locator("#work article [class*=projectEyebrow]") self.assertEqual(category_labels.count(), 4) self.assertTrue( category_labels.evaluate_all( @@ -1525,21 +1517,19 @@ def test_navigation_and_bordered_controls_keep_their_visual_hierarchy(self) -> N } def assert_homebot_image_fits(panel) -> None: - illustration = panel.locator('svg[aria-hidden="true"]').first + illustration = panel.locator('[class*=projectIllustration] > svg') expect(illustration).to_be_visible() self.assertGreater(illustration.bounding_box()["width"], 100) self.assertEqual(panel.locator("video, img").count(), 0) for project, href in project_hrefs.items(): - button = page.get_by_role("button", name=re.compile(project, re.I)) - button.click() - self.assertEqual(button.get_attribute("aria-pressed"), "true") panel = page.locator("article").filter( has=page.get_by_role("heading", name=project, exact=True) ) + panel.scroll_into_view_if_needed() panel.wait_for(state="visible") self.assertEqual( - panel.get_by_role("link", name="View project", exact=True).get_attribute( + panel.get_by_role("link", name=f"View project {project}", exact=True).get_attribute( "href" ), href, @@ -1554,18 +1544,16 @@ def assert_homebot_image_fits(panel) -> None: )).to_be_visible() for tag in ("Read + act", "Marked inferences", "Source links"): expect(panel.locator("[class*='proofRow']").get_by_text(tag, exact=True)).to_be_visible() - expect(panel.locator('svg[aria-hidden="true"]').first).to_be_visible() - link = panel.get_by_role("link", name="View project", exact=True) + expect(panel.locator('[class*=projectIllustration] > svg')).to_be_visible() + link = panel.get_by_role("link", name=f"View project {project}", exact=True) expect(link).to_have_attribute("target", "_blank") expect(link).to_have_attribute("rel", "noreferrer") page.set_viewport_size({"width": 390, "height": 844}) - homebot_button = page.get_by_role("button", name=re.compile("HomeBot", re.I)) - homebot_button.click() - self.assertEqual(homebot_button.get_attribute("aria-pressed"), "true") homebot_panel = page.locator("article").filter( has=page.get_by_role("heading", name="HomeBot", exact=True) ) + homebot_panel.scroll_into_view_if_needed() homebot_panel.wait_for(state="visible") assert_homebot_image_fits(homebot_panel) browser.close() @@ -1586,8 +1574,9 @@ def test_pointer_stirs_the_pond(self) -> None: self.assertEqual( page.evaluate("getComputedStyle(document.querySelector('#top')).userSelect"), - "none", + "auto", ) + self.assertEqual(page.locator('[data-pond-state]').evaluate('element => getComputedStyle(element).userSelect'), 'none') self.assertEqual(page.get_by_text("Touch the water", exact=True).count(), 0) pond = page.locator("[data-pond-state]") @@ -1716,15 +1705,15 @@ def test_food_drop_reservation_and_feeding(self) -> None: }, { once: true })""" ) page.mouse.click(fish_x, fish_y, button="right") + self.assertEqual(page.locator("html").get_attribute("data-pond-context-prevented"), "false") + self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), food_before) + page.mouse.click(fish_x, fish_y) wait_for_condition( page, "initial => Number(document.querySelector('[data-pond-state]')?.dataset.foodDroppedCount) > initial", arg=food_before, ) - self.assertEqual( - page.locator("html").get_attribute("data-pond-context-prevented"), - "true", - ) + self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), food_before + 1) camera_x, camera_y, scale_x, scale_y = ( float(value) for value in pond.get_attribute("data-pond-camera").split(",") ) @@ -1825,7 +1814,7 @@ def test_reduced_motion_preserves_the_ecosystem_at_low_speed(self) -> None: for point in pond.get_attribute("data-fish-positions").split(";") ] fish_x, fish_y = find_open_water(page, pond, fish_points) - page.mouse.click(fish_x, fish_y, button="right") + page.mouse.click(fish_x, fish_y) wait_for_condition( page, "Number(document.querySelector('[data-pond-state]')?.dataset.foodDroppedCount) > 0" @@ -1852,52 +1841,14 @@ def test_mobile_touch_disturbs_the_shared_pond(self) -> None: for point in pond.get_attribute("data-fish-positions").split(";") ] fish_x, fish_y = find_open_water(page, pond, fish_points) - page.evaluate("""() => { - window.__pondTouchEvents = []; - for (const type of ['pointerdown', 'pointerup', 'pointercancel']) { - document.addEventListener(type, event => { - if (event.pointerType === 'touch') window.__pondTouchEvents.push({ - type, time: performance.now(), x: event.clientX, y: event.clientY, - target: event.target?.tagName, - }); - }, true); - } - }""") impacts_before = int(pond.get_attribute("data-primary-impact-count")) + food_before = int(pond.get_attribute("data-food-dropped-count")) page.touchscreen.tap(fish_x, fish_y) - wait_for_condition( - page, - "document.querySelector('[data-pond-state]')?.dataset.touchGesture === 'single-tap-impact'" - ) - self.assertEqual( - int(pond.get_attribute("data-primary-impact-count")), - impacts_before + 1, - ) + wait_for_condition(page, "document.querySelector('[data-pond-state]')?.dataset.touchGesture === 'tap-food'") + wait_for_condition(page, "before => Number(document.querySelector('[data-pond-state]').dataset.foodDroppedCount) === before + 1", arg=food_before) + self.assertEqual(int(pond.get_attribute("data-primary-impact-count")), impacts_before + 1) self.assertGreater(int(pond.get_attribute("data-ring-count")), 0) - self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), 0) - impacts_before_food = int(pond.get_attribute("data-primary-impact-count")) - touch_session = context.new_cdp_session(page) - - def double_tap(x, y): - touch_session.send("Input.synthesizeTapGesture", { - "x": x, "y": y, "duration": 100, - "tapCount": 2, "gestureSourceType": "touch", - }) - - double_tap(fish_x, fish_y) - try: - wait_for_condition( - page, - "Number(document.querySelector('[data-pond-state]')?.dataset.foodDroppedCount) > 0" - ) - except PlaywrightTimeoutError: - detail = pond.evaluate( - "element => ({ gesture: element.dataset.touchGesture, requested: element.dataset.foodRequestedAt, frame: element.dataset.frame, state: element.dataset.pondState })" - ) - detail["touch_events"] = page.evaluate("window.__pondTouchEvents") - context.close() - browser.close() - self.fail(f"Double-tap did not commit food: {detail}") + self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), food_before + 1) camera_x, camera_y, scale_x, scale_y = ( float(value) for value in pond.get_attribute("data-pond-camera").split(",") ) @@ -1913,16 +1864,19 @@ def double_tap(x, y): page.wait_for_timeout(500) self.assertEqual( int(pond.get_attribute("data-primary-impact-count")), - impacts_before_food, + impacts_before + 1, ) dropped_count = int(pond.get_attribute("data-food-dropped-count")) - project_button = page.get_by_role("button", name=re.compile("Rakazo", re.I)) - project_button.scroll_into_view_if_needed() - box = project_button.bounding_box() - double_tap(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2) + project_link = page.get_by_role("link", name="Things I’ve made", exact=True) + project_link.scroll_into_view_if_needed() + box = project_link.bounding_box() + page.touchscreen.tap(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2) page.wait_for_timeout(500) self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), dropped_count) + page.evaluate("window.scrollTo(0, 0)") + fish_x, fish_y = find_open_water(page, pond) + def pointer_sequence(events): page.evaluate( """({ x, y, events }) => { @@ -1970,7 +1924,7 @@ def pointer_sequence(events): self.assertEqual(int(pond.get_attribute("data-food-dropped-count")), dropped_count) self.assertEqual( int(pond.get_attribute("data-primary-impact-count")), - impacts_before_food, + impacts_before + 1, ) self.assertGreaterEqual(int(pond.get_attribute("data-fish-count")), 8) context.close() diff --git a/tests/browser/issues_test.py b/tests/browser/issues_test.py index c45154c..b6797b8 100644 --- a/tests/browser/issues_test.py +++ b/tests/browser/issues_test.py @@ -1,5 +1,5 @@ """Behaviour checks for the ready homepage issue batch.""" -from browser_test import BrowserTestCase, expect, wait_for_condition +from browser_test import BrowserTestCase, expect, wait_for_condition, find_open_water class HomepageIssueTests(BrowserTestCase): def test_skip_link_is_first_and_focuses_main(self): @@ -24,7 +24,7 @@ def test_actions_captions_privacy_and_immediate_navigation(self): self.assertGreaterEqual(link.evaluate('e=>parseFloat(getComputedStyle(e).fontSize)'),14) self.assertGreaterEqual(link.bounding_box()['height'],44) expect(page.locator('footer').get_by_role('link',name='Privacy')).to_have_attribute('href','/privacy') - for section in ('Work','About','Contact'): + for section in ('Work','More','Contact'): page.get_by_role('navigation',name='Portfolio navigation').get_by_role('link',name=section,exact=True).click() expect(page.locator('header [aria-current="location"]')).to_have_text(section) self.assertEqual(page.locator('#contact').evaluate('e=>getComputedStyle(e).opacity'),'1') @@ -35,7 +35,7 @@ def test_actions_captions_privacy_and_immediate_navigation(self): self.assertEqual(page.locator('#contact').evaluate('e=>getComputedStyle(e).opacity'),'1') page.close() - def test_feed_button_keyboard_and_ascii_without_unused_rasters(self): + def test_pointer_feeding_without_feed_control_or_unused_ascii_rasters(self): browser = self.playwright.chromium.launch() self.addCleanup(browser.close) page = self.pond_page(browser) @@ -47,9 +47,11 @@ def test_feed_button_keyboard_and_ascii_without_unused_rasters(self): for mode in ('ASCII','Pixel'): page.get_by_label(mode,exact=True).check() wait_for_condition(page, "document.querySelector('[data-pond-state]')?.dataset.pondState==='running'") - feed=page.get_by_role('button',name='Feed fish',exact=True) - expect(feed).to_be_enabled() - before=int(page.locator('[data-pond-state]').get_attribute('data-food-dropped-count')) - feed.focus();page.keyboard.press('Enter') - wait_for_condition(page, "before=>Number(document.querySelector('[data-pond-state]').dataset.foodDroppedCount)>before",arg=before) - expect(page.get_by_role('status',name='').filter(has_text='Food dropped')).to_be_visible() + self.assertEqual(page.get_by_role('button',name='Feed fish',exact=True,include_hidden=True).count(),0) + self.assertEqual(page.locator('[class*="feedControl"], [class*="feedButton"]').count(),0) + pond=page.locator('[data-pond-state]') + before=int(pond.get_attribute('data-food-dropped-count')) + x,y=find_open_water(page,pond) + page.mouse.click(x,y) + wait_for_condition(page, "before=>Number(document.querySelector('[data-pond-state]').dataset.foodDroppedCount)===before+1",arg=before) + self.assertEqual(int(pond.get_attribute('data-food-dropped-count')),before+1)