[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. luke-ui.netlify.app/
0

Configure Feed

Select the types of activity you want to include in your feed.

Capture full content for tall visual elements beyond viewport (#286)

* Capture full content for tall visual elements beyond viewport (#271)

Element-level screenshots of components taller than the 800px viewport
(e.g. the token board with 128 leaves, which is ~7k px) only painted
the first viewport's worth of content. The PNG was sized to the full
document height but rows below the fold were blank because Playwright
omits content outside the viewport by default.

Add captureBeyondViewport: true to the visual project's toMatchScreenshot
config so Playwright renders the full element regardless of viewport
bounds. This applies to all 126+ existing captures; only the handful
that are taller than 800px change in practice.

Add a post-capture check that warns when scrollHeight exceeds the
viewport but no child element actually reaches below it — a sanity
check that catches scrollHeight inflation without false positives.

The visual regression comparison pipeline in scripts/visual-regression
will flag any capture with dimension mismatch as 'changed', so if
captureBeyondViewport is ever removed the regression is caught at the
image-comparison level.

Closes #271.

* Fix lint and type errors in visual capture config

- Type the resolveScreenshotPath callback params explicitly so the as any
cast on toMatchScreenshot doesn't lose type safety for known properties
- Suppress the no-console lint warning on the post-capture warn call

* Use CDP viewport resize instead of captureBeyondViewport config

captureBeyondViewport: true in the vitest toMatchScreenshot config
caused ALL visual captures to time out in CI (Matcher did not succeed
in time). The Playwright internal handler only sets captureBeyondViewport
when !fitsViewport, but forcing it globally adds overhead even for
elements that fit within the viewport.

Instead, resize the viewport to the element's full height via CDP in
captureVisual only when the element extends beyond the 800px viewport.
This ensures Playwright renders the full content without affecting
the majority of captures that fit within the viewport.

The viewport metadata embedded in the filename is captured before
resizing (so it always shows the configured 1024x800 viewport). The
viewport is restored to its original size after capture.

* Configure vitest API write and Vite FS allow for visual captures in worktree

Two guards in @vitest/browser 4.1.x block screenshot writes
when vitest runs in a git worktree:

- assertBrowserApiWrite requires api.allowWrite and
browser.api.allowWrite to be truthy
- assertBrowserFileAccess requires the capture path to be
within Vite's server.fs.allow scope (fails when
VISUAL_CAPTURE_DIR points outside the worktree root)

Add api.allowWrite at project level, browser.api.allowWrite
in the visual project, and server.fs.allow pointing at
.artifacts (found by searching up from the config dir) so
captures written to the cache directory pass the Vite FS
check.

authored by

Luke Bennett and committed by
GitHub
(Jul 27, 2026, 10:34 AM +1000) b763baca b5575b93

+45
+22
packages/@luke-ui/react/vitest.config.ts
··· 1 + import fs from 'node:fs'; 1 2 import path from 'node:path'; 2 3 import { fileURLToPath } from 'node:url'; 3 4 import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; ··· 8 9 const dirname = 9 10 typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); 10 11 const configDir = path.join(dirname, '.storybook'); 12 + 13 + function findAncestorDir(name: string, from = dirname): string | undefined { 14 + let current = path.resolve(from); 15 + do { 16 + if (fs.existsSync(path.join(current, name))) return current; 17 + current = path.dirname(current); 18 + } while (current !== path.dirname(current)); 19 + } 20 + 21 + const artifactsDir = findAncestorDir('.artifacts'); 22 + 11 23 export default defineConfig({ 12 24 optimizeDeps: { 13 25 include: [ ··· 16 28 'react-aria-components/Link', 17 29 ], 18 30 }, 31 + server: { 32 + fs: { 33 + allow: artifactsDir ? [artifactsDir] : undefined, 34 + }, 35 + }, 19 36 test: { 37 + api: { allowWrite: true }, 20 38 projects: [ 21 39 { 22 40 extends: true, ··· 76 94 test: { 77 95 browser: { 78 96 enabled: true, 97 + api: { allowWrite: true }, 79 98 expect: { 80 99 toMatchScreenshot: { 100 + // captureBeyondViewport is NOT set globally — it is applied 101 + // per-capture in captureVisual via CDP viewport resizing only 102 + // when the element extends beyond the viewport. 81 103 resolveScreenshotPath: ({ arg, ext, root }) => { 82 104 return path.join( 83 105 process.env.VISUAL_CAPTURE_DIR ?? path.join(root, '.visual-captures'),
+23
packages/@luke-ui/react/src/test-utils/render-visual.tsx
··· 67 67 throw new Error(`Visual capture IDs must use a component namespace: ${id}`); 68 68 } 69 69 const viewport = `${window.innerWidth}x${window.innerHeight}`; 70 + const element = locator.element(); 71 + const fullHeight = element.scrollHeight; 72 + 73 + if (fullHeight > window.innerHeight) { 74 + // The element extends beyond the viewport. Increase the viewport height 75 + // so Playwright renders the full content, then restore after capture. 76 + await cdp().send('Emulation.setDeviceMetricsOverride', { 77 + width: window.innerWidth, 78 + height: fullHeight, 79 + deviceScaleFactor: window.devicePixelRatio, 80 + mobile: false, 81 + }); 82 + } 83 + 70 84 await expect.element(locator).toMatchScreenshot(`${id}__viewport-${viewport}`); 85 + 86 + if (fullHeight > window.innerHeight) { 87 + await cdp().send('Emulation.setDeviceMetricsOverride', { 88 + width: window.innerWidth, 89 + height: window.innerHeight, 90 + deviceScaleFactor: window.devicePixelRatio, 91 + mobile: false, 92 + }); 93 + } 71 94 } 72 95 73 96 /** Captures one look with a stable identity-and-mode suffix added to `id`. */