From 71a5860ed9cbe8aaf4e1ce4623e1a6e08cf52d8a Mon Sep 17 00:00:00 2001 From: LuLaValva Date: Wed, 19 Aug 2026 20:22:19 -0700 Subject: [PATCH 1/6] feat: accept pre-parsed style rules Reading and specificity sorting every rule in the document is the bulk of a capture's work, and it repeats per call even when nothing about the document has changed. Options.styleRules lets a suite parse once, via the now exported getDocumentStyleRules, and reuse the result. Omitting it keeps the existing behaviour. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 19 ++++++++++++++++++- src/__tests__/index.ts | 18 +++++++++++++++++- src/index.ts | 4 +++- src/types.ts | 2 ++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 63e2333..5c178eb 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,30 @@ Check out the [tests](./src/__tests__/index.ts) for some examples. ## API -### `visualHTML(div: Element, options?: { shallow?: boolean })` +### `visualHTML(div: Element, options?: { shallow?: boolean, styleRules?: SelectorWithStyles[] })` ```javascript visualHTML(document.body); // Returns the visual information of all nested elements in the body. visualHTML(document.body, { shallow: true }); // Returns just visual information for the `` element. ``` +### `getDocumentStyleRules(document: Document)` + +Every capture reads and specificity sorts the document's style rules, which is +the bulk of its work. A suite capturing many elements against the same +stylesheets can do that once and hand the result to each call: + +```javascript +const styleRules = getDocumentStyleRules(document); + +for (const el of elements) { + snapshot(visualHTML(el, { styleRules })); +} +``` + +Media conditions are evaluated as the rules are read, so parse again whenever +the viewport changes. + ## How it works `visual-html` works by building up an HTML representation of the DOM including only attributes that account for the visual display of the element. diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index 5abcf01..c1a6210 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -1,4 +1,4 @@ -import visualHTML from ".."; +import visualHTML, { getDocumentStyleRules } from ".."; const { matchMedia: _matchMedia } = window; const { supports: _supports } = @@ -370,3 +370,19 @@ function testHTML(html: string, styles: string = "") { document.head.removeChild(style); return result; } + +test("styles an element from pre-parsed rules after its stylesheet is gone", () => { + const style = document.createElement("style"); + style.innerHTML = ".parsed { color: green; }"; + document.head.appendChild(style); + const div = document.createElement("div"); + div.className = "parsed"; + document.body.appendChild(div); + + const styleRules = getDocumentStyleRules(document); + document.head.removeChild(style); + const result = visualHTML(div, { styleRules }); + document.body.removeChild(div); + + expect(result).toMatchInlineSnapshot(`"
"`); +}); diff --git a/src/index.ts b/src/index.ts index d627813..d50fff4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { } from "./stylesheets"; export { VisualData, Options }; +export { getDocumentStyleRules }; const ELEMENT_TYPE = 1; const TEXT_TYPE = 3; @@ -20,7 +21,8 @@ export default function visualHTML(el: Element, options: Options = {}) { return stringifyVisualData( getVisualData(el, { ...options, - styleRules: getDocumentStyleRules(el.ownerDocument!), + styleRules: + options.styleRules ?? getDocumentStyleRules(el.ownerDocument!), }) ); } diff --git a/src/types.ts b/src/types.ts index fe11257..5dda14b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,8 @@ export interface VisualData { } export interface Options { shallow?: boolean; + /** Rules from `getDocumentStyleRules`, to reuse across captures. */ + styleRules?: SelectorWithStyles[]; } export interface SelectorWithStyles { From 8c3dce45438d8297f526e5d26677fe97fe2c3053 Mon Sep 17 00:00:00 2001 From: LuLaValva Date: Wed, 19 Aug 2026 20:22:19 -0700 Subject: [PATCH 2/6] fix: read derived attributes from their content attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several of the whitelisted properties report a value the author never wrote. URL properties (src, poster, data, background) resolve against the document's base URL, so the same markup serialises differently depending on where it was served from — under a dev server on a random port, differently between runs. An 's width and height report its natural size once the image has loaded, and currentSrc is empty until then, so output also depended on whether a network fetch had finished. These now read their content attribute, recording what the author wrote. An absent attribute contributes nothing: the rendered size of an image is already covered by its styles. The currentSrc entry, which existed only to capture , is merged into the src entry, and srcset joins the whitelist so a responsive image still reports its candidates. Snapshots holding any of these attributes will need regenerating. Co-Authored-By: Claude Opus 5 (1M context) --- src/__tests__/index.ts | 48 ++++++++++++++++++++++++++++++++++++++++++ src/attributes.ts | 8 ++++--- src/html-properties.ts | 26 ++++++++++++++++++----- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index c1a6210..fc318cd 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -386,3 +386,51 @@ test("styles an element from pre-parsed rules after its stylesheet is gone", () expect(result).toMatchInlineSnapshot(`"
"`); }); + +test("keeps urls as authored rather than resolved against the document", () => { + expect( + testHTML(` +