Export editable screenshot layers - #2049
Conversation
📝 WalkthroughWalkthroughScreenshot scenarios can skip admin setup and optionally generate transparent background, controls, and text layers. The capture pipeline adds layer masking, deterministic timing, element cropping, PNG padding, CLI validation, lazy server startup, and schema validation. ChangesScreenshot layering
Screenshot runtime infrastructure
Built-site blob validation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ScreenshotCLI
participant captureScenario
participant capturePreparedLayers
participant withScreenshotLayer
participant PNGUtilities
ScreenshotCLI->>captureScenario: forward layers option
captureScenario->>capturePreparedLayers: capture configured layers
capturePreparedLayers->>withScreenshotLayer: apply layer stylesheet and paint markers
withScreenshotLayer->>capturePreparedLayers: return masked PNG
capturePreparedLayers->>PNGUtilities: crop element layer
PNGUtilities-->>captureScenario: return layer PNG record
captureScenario-->>ScreenshotCLI: write layer PNG files
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/screenshots/layers.ts`:
- Around line 12-13: Update CONTROL_LETTERING_SELECTOR to include select so
selected values are excluded from overlapping layers. Extend the text-layer
styling rules to clear relevant visual properties on ::before and ::after,
preventing pseudo-element backgrounds, borders, and shadows from leaking into
the text layer. Add rendering regression cases covering a select control and
pseudo-element decoration, each reproducing the overlap and failing before the
fix.
In `@scripts/screenshots/scenario.ts`:
- Around line 29-30: Update the screenshot scenario loading boundary to define
and use a Valibot schema for the scenario shape, parse loaded.default, and
return the parsed result before passing it to captureScenario. Move the
elementSelector/fullPage cross-field validation to run after parsing, preserving
that existing check against the validated data.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b3b48288-2169-4439-9ab7-ab00b10b5148
📒 Files selected for processing (13)
README.mdscripts/screenshots.tsscripts/screenshots/capture.tsscripts/screenshots/image.tsscripts/screenshots/layers.tsscripts/screenshots/options.tsscripts/screenshots/scenario.tstest/fixtures/screenshots/scenarios/invalid-admin-setup.tstest/fixtures/screenshots/scenarios/with-username.tstest/scripts/screenshots-capture.test.tstest/scripts/screenshots-image.test.tstest/scripts/screenshots-options.test.tstest/scripts/screenshots-scenario.test.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cacaee2bc4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/screenshots/layers.ts`:
- Around line 17-25: Update the masking CSS in scripts/screenshots/layers.ts at
lines 17-25 and 34-39: add transparent -webkit-text-stroke-color and
text-decoration-color alongside the existing text color masking for both body
text and control lettering. Add browser regression cases covering explicit
stroke and decoration colors.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f8c373b1-a65e-439c-bd27-0ebce30226c4
📒 Files selected for processing (4)
scripts/screenshots/layers.tsscripts/screenshots/scenario.tssrc/shared/db/built-sites/blob.tstest/scripts/screenshots-browser.contract.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cab8079281
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac39b84f7c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7d3dd1ff8c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8da8c5c68b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 76d7c6b8a6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/screenshots/layers.ts`:
- Around line 41-45: Extend the masking selector rule in the screenshot layer
styling to include descendant pseudo-elements via *::before and *::after,
ensuring nested control content such as button span::after is hidden. Add a
browser regression test covering explicit text paint on a descendant
pseudo-element, and verify it fails without the selector change and passes with
it.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 1220010f-b49c-443d-a823-4cdc4bad1e84
📒 Files selected for processing (10)
deno.jsonscripts/screenshots.tsscripts/screenshots/capture.tsscripts/screenshots/image.tsscripts/screenshots/layers.tstest/scripts/screenshots-browser-helpers.tstest/scripts/screenshots-browser.contract.tstest/scripts/screenshots-capture.test.tstest/scripts/screenshots-image.test.tstest/scripts/screenshots-layers-browser.contract.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2a8ffe5f7f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 836a890c3b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a46121d57a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b644ade1b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b76140c524
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b33a4e2390
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
# Conflicts: # scripts/mutation/isolation.ts
There was a problem hiding this comment.
Actionable comments posted: 15
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/screenshots/layers.ts`:
- Around line 327-344: Update visitRoot so shadow-root traversal passes the
effective paint mark, falling back to inheritedPaint when paintMark(element)
returns null; preserve the host’s own mark when present. Add a layer contract
case covering an opacity group containing a shadow host and assert the shadow
content receives the inherited mark.
- Line 52: Update the controls background reset rule in the layer stylesheet to
append the existing LAYER_PRIORITY suffix to the html/body selector, matching
the other rules and ensuring it outranks competing important cascade-layer
rules.
In `@src/ui/static/style.scss`:
- Line 5: Document the required coupling above the `@layer` declaration by adding
a short comment stating that the literal __screenshot_layer__ must remain
identical to SCREENSHOT_LAYER in scripts/screenshots/layers.ts. Leave the layer
name and declaration unchanged.
In `@test/scripts/screenshots-boundaries-browser.contract.ts`:
- Around line 34-44: Update the shadowStyle layer parameter to use the existing
exported ScreenshotLayerName type from screenshots-browser-helpers.ts instead of
the duplicated inline union. Confirm the type’s export and import path, then
reuse it without changing the helper’s behavior.
In `@test/scripts/screenshots-browser-helpers.ts`:
- Around line 20-38: Update countRgbPixels to include the alpha channel in its
per-pixel match, counting a pixel only when its alpha value is nonzero in
addition to matching the requested RGB values. Preserve the existing RGB
matching and channel iteration behavior.
In `@test/scripts/screenshots-capture.test.ts`:
- Around line 145-152: Update the document mock’s querySelectorAll
implementation to retain only the "*" branch returning elements and the
link[...] branch returning an empty array; remove the unreachable "html, body,
body *" condition and the attributes-based fallback.
- Around line 235-248: Update the screenshot assertions to import and reuse the
production SCREENSHOT_LAYER_NAMES constant: replace the hard-coded expected
layer-name array with [...SCREENSHOT_LAYER_NAMES], and derive the style-removal
expectation from SCREENSHOT_LAYER_NAMES.length + 1. Keep the existing assertion
ordering and other expectations unchanged.
In `@test/scripts/screenshots-compositing-browser.contract.ts`:
- Around line 211-217: Replace the fixed setTimeout delay between the two ticks
reads with page.waitForFunction that polls until globalThis.ticks differs from
beforeAdvance. Preserve the existing beforeAdvance capture and final inequality
assertion, and avoid any real-time sleep.
- Around line 152-155: Replace the setTimeout-based delay in the page.route
handler for the screenshots.test/value request with a manually controlled
promise gate that keeps the request pending until the test explicitly releases
it. Add a concise comment documenting that the gate is required to validate the
networkidle contract, while preserving the existing Ready response after
release.
In `@test/scripts/screenshots-layers-browser.contract.ts`:
- Around line 136-141: Update the color assertion in the CSP test callback
around layerStyle to use toBe with the deterministic masked color value instead
of not.toBe("rgb(255, 0, 0)"). Preserve the existing layerStyle invocation and
assert the exact expected result.
- Around line 265-297: Extract the duplicated
addScreenshotStyle/assert/layerStyle/cleanup flow from the tests into a shared
helper accepting the CSS text and expected colour. Update both “preserves
imports in screenshot CSS” and “adds screenshot CSS on a page with a URL
fragment” to invoke that helper while preserving their existing page setup and
assertions.
In `@test/scripts/screenshots-layers.test.ts`:
- Around line 155-190: Move global restoration out of the mocked page.unroute
implementation: have setupPage return the created globals stash alongside its
existing page and observable helpers, then restore that stash from an afterEach
hook covering the tests. Keep unroute focused on setting removedRoute and
returning its promise, preserving unrouted as the signal that cleanup ran.
In `@test/scripts/screenshots-scrollbars-browser.contract.ts`:
- Around line 12-18: Replace the local Chromium launch in the scrollbar contract
with a launcher exported from screenshots-browser-helpers.ts. Extend
defineScreenshotBrowserLauncher to accept the ignoreDefaultArgs override, create
a second shared launcher configured with ["--hide-scrollbars"], and have this
test use it while preserving the standard launchScreenshotBrowser behavior.
In `@test/scripts/screenshots-server.test.ts`:
- Line 47: In the cleanup sequence, replace the optional invocation of
cleanups[0] with a direct call, since the preceding setup guarantees it exists.
Preserve the existing cleanup ordering while allowing invalid state to fail
immediately.
In `@test/scripts/stripe-mock/ports.test.ts`:
- Around line 28-40: Update the “keeps a reserved port unavailable until
release” test to call reserved.release() twice before rebinding. Store the
Deno.listen result and close it in a finally block, then release the reservation
there as cleanup, while preserving the existing assertion that binding fails
before release.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8a99039f-4165-44ca-a23e-b7d5992f6ea0
📒 Files selected for processing (21)
scripts/screenshots.tsscripts/screenshots/app-server.tsscripts/screenshots/capture.tsscripts/screenshots/layers.tsscripts/screenshots/readiness.tsscripts/screenshots/server.tsscripts/stripe-mock.tssrc/ui/static/style.scsstest/scripts/screenshots-boundaries-browser.contract.tstest/scripts/screenshots-browser-helpers.tstest/scripts/screenshots-browser.contract.tstest/scripts/screenshots-capture-clock.test.tstest/scripts/screenshots-capture.test.tstest/scripts/screenshots-compositing-browser.contract.tstest/scripts/screenshots-edge-layers-browser.contract.tstest/scripts/screenshots-layers-browser.contract.tstest/scripts/screenshots-layers.test.tstest/scripts/screenshots-readiness.test.tstest/scripts/screenshots-scrollbars-browser.contract.tstest/scripts/screenshots-server.test.tstest/scripts/stripe-mock/ports.test.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5d6f1a44bf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e78e7dfea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 388c8c72d7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 66570ddb1b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
What changed
External screenshot scenarios can export separate background, controls, and text PNG files. The files reproduce the companion normal screenshot when stacked, so each part can be edited without changing the final image.
Layering handles native and hidden controls, selection highlights, generated text and images, clipped paint, border images, scrollbars, SVG and embedded content, open shadow roots, CSS masks, and grouped paint such as opacity, filters, backdrop filters, and blend modes. Pages with modal dialogs or popovers stay together so translucent overlays keep the browser paint order. Direct text under page and shadow roots also stays unchanged without adding styleable wrapper elements.
Capture waits for pending page requests before it starts. Page timers, animation callbacks, CSS animations, and transitions pause while the three editable layers are captured. Paint marks are installed once for the complete sequence, so page observers cannot change visible state between frames. The companion normal PNG is built from those exact frames, so video, animated images, and other media cannot show a different moment. Element captures use one preliminary image only to calculate shared crop bounds.
Temporary screenshot styles load from the same site under the production content security policy and inside open shadow roots. Cleanup runs after success or failure, and reports both the capture and cleanup errors when both fail. Scenarios can capture the site before admin setup without starting Stripe, and app ports stay reserved until each process starts.
Checks
nix develop -c deno task test:screenshot-contract(59 browser steps)nix develop -c deno task precommitnix develop -c deno task mutation src/shared/db/built-sites/blob.ts test/shared/db/built-sites/blob.test.ts --harness(17/17 mutants killed)