| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix: freeze the image the browser actually shows (srcset descriptors, type filters, content:url) | 1 个月前 | |
exposed internal visual demos for SnapDOM testing and add new tests | 10 天前 | |
fix: prefetch mask and border-image URLs in preCache, drop the dead warm call | 1 个月前 | |
update tests | 1 个月前 | |
perf: replace Safari's 3x pre-capture warmup with a verified draw at raster time | 1 个月前 | |
perf: replace Safari's 3x pre-capture warmup with a verified draw at raster time | 1 个月前 | |
test: use importOriginal in vi.mock factories (fixes flaky WebKit runs) | 1 个月前 | |
exposed internal visual demos for SnapDOM testing and add new tests | 10 天前 | |
exposed internal visual demos for SnapDOM testing and add new tests | 10 天前 | |
feat: clip option + snapdom.viewport() — region capture with offscreen culling clip: 'viewport' | {x,y,width,height} captures a page region ~24x faster than full-page by pruning offscreen subtrees into layout-preserving husks during deepClone (no styling/inlining/raster work for culled content) and windowing the viewBox to the clip rect resolved once at cull time. - Culling is escape-aware: descendants painting into the window (fixed widgets, portal-less modals, negative offsets, transforms) veto the cull; rtl/vertical-writing overflow expands the test box; replaced inline elements (img/canvas/...) are cullable so offscreen fetches are skipped. - freezeViewportPositioned replaces freezeSticky everywhere: fixed/sticky clones re-anchor to their painted gBCR position and hoist to the clone root (escapes #364 scroll wrappers, overflow clipping and CB-forming ancestors); sticky leaves an in-flow placeholder; individual rotate/scale compose via DOMMatrix; shadow-scoped clones freeze in place. Also fixes stuck sticky/fixed inside scrolled element captures (d33: header/footer/left-sidebar under two-axis scroll + inner scroller state). - Chromium 148/149 workaround: an absolute box exactly on the window edge makes a later positioned sibling vanish in SVG-image rasterization; nudge 1px outside the window (clipped away) to break the coincidence. - Clip mode skips the clone-in-document measurement pass, scrollHeight expansion, forceContentVisibility, and prunes lineClamp/font-collection walks by window intersection; root bleed is never added to a clip window. - Non-clip hot path unchanged (within benchmark noise). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
feat(compress): downsample CSS backgrounds and SVG <image> too Extend the same downsampling logic beyond <img> to every inlined raster: - CSS background-image: downsample to the element's visible box, but only for non-repeating backgrounds (a background clips to the box, so the box bounds what's visible regardless of background-size; tiled backgrounds are skipped since a small repeated tile needs its natural resolution). Element box read from the original node via the session nodeMap. - SVG <image href>: downsample to its rendered width/height attrs. New exports compressClonedBackgrounds / compressClonedSvgImages + a compressCloneAssets wrapper (capture.js now calls the wrapper). Verified in a real capture: a no-repeat background gallery shrinks the SVG ~46%. Tests: background downsample, tiled-background skip, SVG <image> downsample. Benchmark: background-image gallery (toCanvas) added to compress.benchmark.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 2 个月前 | |
fix: capture roots that are themselves the target element were skipped querySelectorAll never matches the element it's called on, so calling snapdom() directly on an <img>, <picture>, a Material icon span, a <canvas>, or a blob: image (no wrapper) silently skipped compress, icon-font ligature conversion, picture/lazy resolution, Safari canvas warmup, and blob URL resolution for that root — only descendants were ever checked. Same shape as #461. Adds a small selfAndDescendants / root.matches() check at each of those call sites. | 1 个月前 | |
fix(cache): invalidate snapshot on option change, reset measureHints on disabled Two cross-capture cache-correctness bugs: - getSnapshot keyed snapshotCache only by element + __epoch, but the snapshot content depends on embedFonts (extra font props) and excludeStyleProps (skipped props). __epoch only bumps on DOM/font mutation, so capturing the same element twice with different options returned the stale snapshot, silently breaking excludeStyleProps (#348) and embedFonts on repeat captures. Snapshot is now invalidated when either option changes (excludeStyleProps by reference: a fresh value misses safely). - applyCachePolicy('disabled') reinstantiated every persistent cache except measureHints (the clone-in-document layout WeakMap), so cache:'disabled' still leaked stale layout measurements between captures. Reset it too. Also corrects the measureHints value-shape comment ({ cssLen, w0, csh, csw }). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
fix(styles): disable animations on clones so entry keyframes don't blank the capture (#476) snapdom embeds the document's `@keyframes` and re-applies each element's computed `animation` (via its generated style class). When the serialized SVG is rasterized as an image, that CSS animation replays from its 0% keyframe. For an entry animation whose start frame hides the element (e.g. `from { opacity: 0 }`, or a large off-screen `transform`), the element renders blank/mispositioned in the snapshot — even though the live element already finished animating and is fully visible. Fix: for elements that actually have an animation (`animation-name !== none`), pin `animation: none !important` on the clone. Inline `!important` beats the generated class regardless of specificity, and the element's current opacity/transform is already captured in the style snapshot, so it renders as the frozen, settled frame. The guard keeps non-animated elements (and detached nodes whose computed `animation-name` is empty) untouched. Adds a regression test that fades a fixed element in (opacity 0→1), waits for the animation to finish, then asserts the captured pixel is opaque. Co-authored-by: 娄家豪 <loujiahao@bytedance.com> | 1 个月前 | |
feat: add burst:true capture memoization, replacing session() Repeated captures of an unchanged element (dashboard polling, video/gif frame loops) skip the pipeline entirely and return a memoized result. State lives in a WeakMap keyed by element instead of an explicit session handle — no dispose() needed. Warns once via console when the same element is captured 3+ times in 2s without burst:true. | 1 个月前 | |
Make the test suite pass on Firefox and WebKit The suite was Chromium-shaped in several places; no product code changes: - replace the corrupt 1x1 PNG fixtures (bad zlib checksum in IDAT): Chromium/WebKit decode them leniently, Firefox rejects with EncodingError - restore globalThis.Image after the Safari toImg branch test; the leaked stub resolved decode() before load, which happened to work on Chromium but produced 0-sized canvases (null blobs) on Firefox - gate engine-specific behavior: WebP encode falls back to PNG where unsupported, Safari keeps natural svg size for width/height options, Firefox swaps checkbox/radio for the visual replacement, WebKit/Firefox resolve srcset currentSrc eagerly (assert the freeze invariant instead) - accept Gecko serializations: vertical-align exposed via its longhand alignment-baseline in computed-style enumeration, uniform scale as '2', fractional scroll positions, ::-webkit-scrollbar declarations dropped from CSSOM Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
Fix transformed reconciliation. Ref #489 | 17 天前 | |
Fix rotated-root captures clipped at edges (d31) A transformed root's fractional bbox sat flush against the raster edge and several rendering paths shaved the last 1-2px (corners cut). Four changes: - pad the viewBox by 2px per side for any bbox-affecting transform (was Safari-only) so rotated corners never touch the raster boundary - ceil the viewBox so fractional extents are never truncated - foreignObject covers the whole viewBox and the bbox offset moves into container padding: fo overflow doesn't paint in stable Chrome, and CSS position/margin/transform offsets on the fo root double-paint at browser zoom != 100% (negative clip-mode offsets stay on fo x/y) - container spans the fo so content never overflows its border-box (standalone-SVG Chromium clips fo content at the container box) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
lint tests | 11 个月前 | |
fix(capture): disable WebKit text autosizer inside foreignObject. See #327 | 4 个月前 | |
fix: neutralize CSS zoom on the capture root (Ref #483) The raster is sized from offsetWidth/offsetHeight and the generated class uses getComputedStyle() widths, both expressed in the element's own coordinate space, before its own zoom is applied. shouldIgnoreProp already keeps zoom out of the generated class (#369), but an inline zoom survives cloneNode() and gets applied again inside the foreignObject, shrinking the content into the top-left corner and leaving blank right/bottom bands (or overflowing when zoom > 1). Pin the clone root to zoom:1 in the same phase that neutralizes root shadows and margin collapse. Descendants keep their own zoom: their computed sizes are local too, so they still need the scale factor. Adds regression tests for root zoom < 1 and > 1, for the descendant zoom being preserved, and for the helper being a no-op without zoom. | 28 天前 | |
feat(plugins): expose capture geometry, exact export options and canvas cropping Document exporters need to know where the content sits inside the serialized SVG and to rasterize it a region at a time. Three additions, all read-only surface: - result.meta / context.meta: frozen render geometry (viewBox size, logical capture box, contentX/contentY origin, resolved clip window). contentX/Y are the exact origin rather than a centred guess, so they stay correct under asymmetric shadows, transformed roots and clip windows. The property is non-writable so a hook cannot desynchronise it from the canonical SVG, but it stays configurable: options is caller-owned and toPng/toJpg/toWebp pass their raw bag straight to captureDOM, so a reused object must be able to take a second capture's geometry instead of throwing "Cannot redefine property". - export.requestedOptions: a frozen shallow copy of exactly what the caller passed, snapshotted when toXxx() is called rather than when the export reaches the session queue. Callers commonly reuse an options object, and a slow earlier export must not let later mutation rewrite an already-requested one. Key presence is preserved, so a plugin can tell an explicit value from an omitted one even when it equals the capture default. - toCanvas({ crop }): windows the SVG in viewBox coordinates by rewriting the header before decode, so a long capture can be rasterized page by page instead of allocating one bitmap that exceeds the browser decode limits. The crop clips to the intersection with the viewBox and never degrades silently: an empty, non-finite or fully outside window, or a non-SVG payload, rejects with a RangeError rather than returning the whole capture where one page was asked for. The decoded bitmap becomes the aspect reference, otherwise a width-only crop inherits the full document ratio and stretches. | 1 个月前 | |
fix(clone): keep the pre-toDataURL frame for WebGL canvases With preserveDrawingBuffer:false the drawing buffer is cleared once the frame composites, so toDataURL from a plain task reads back transparent. Gate the rAF on a non-2D context instead of on Safari, so WebGL gets its frame and 2D canvases keep the fast path. ref #480 | 1 个月前 | |
Fix offscreen captures. Ref #488 | 17 天前 | |
fix(clone): drop light DOM that no slot accepted A shadow host paints its light DOM only through slots. A child that no slot accepted, either because its slot="name" matches nothing or because the shadow tree has no <slot> at all, is outside the flat tree and paints nothing, yet it was still cloned and appended after the shadow content. That put content in the capture the page never shows, and showed it twice for the common component that reads its own light DOM and renders a copy inside its shadow tree. | 13 天前 | |
fix(styles): reproduce visibility and content-visibility the way browsers do Two rendering-fidelity bugs in the same style snapshot, both verified against real Chromium pixels and then checked on Chromium, WebKit and Firefox. visibility: a hidden ancestor was flattened to opacity:0. visibility is inherited but a descendant can restore it with visibility:visible and still paint; opacity cannot be undone that way, so any such descendant was lost. Keep visibility as visibility. content-visibility:hidden leaked its whole subtree into captures. The pre-clone pass forced both auto and hidden to visible, which erased the value the snapshot guard looked for, so the guard added to stop the leak could never fire. Two unit tests hid this: one exercised inlineAllStyles directly on an element that still carried hidden, the other asserted that forcing hidden was correct. Both passed while the feature was dead end to end. What browsers actually do: the element paints its own box (background, border, padding) and its contents are skipped outright, and a descendant's visibility:visible does NOT bring them back. So neither half of the old code was right, and mapping it to visibility:hidden would erase the box too. Force only auto, which is what the offscreen-skipping issue needed, and carry the content-visibility declaration itself into the snapshot so the rasterizer applies the real semantics natively. Both contradicting tests are updated to the verified behaviour. | 1 个月前 | |
feat: extract pictureResolver into standalone module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 5 个月前 | |
feat(plugins): expose capture geometry, exact export options and canvas cropping Document exporters need to know where the content sits inside the serialized SVG and to rasterize it a region at a time. Three additions, all read-only surface: - result.meta / context.meta: frozen render geometry (viewBox size, logical capture box, contentX/contentY origin, resolved clip window). contentX/Y are the exact origin rather than a centred guess, so they stay correct under asymmetric shadows, transformed roots and clip windows. The property is non-writable so a hook cannot desynchronise it from the canonical SVG, but it stays configurable: options is caller-owned and toPng/toJpg/toWebp pass their raw bag straight to captureDOM, so a reused object must be able to take a second capture's geometry instead of throwing "Cannot redefine property". - export.requestedOptions: a frozen shallow copy of exactly what the caller passed, snapshotted when toXxx() is called rather than when the export reaches the session queue. Callers commonly reuse an options object, and a slow earlier export must not let later mutation rewrite an already-requested one. Key presence is preserved, so a plugin can tell an explicit value from an omitted one even when it equals the capture default. - toCanvas({ crop }): windows the SVG in viewBox coordinates by rewriting the header before decode, so a long capture can be rasterized page by page instead of allocating one bitmap that exceeds the browser decode limits. The crop clips to the intersection with the viewBox and never degrades silently: an empty, non-finite or fully outside window, or a non-SVG payload, rejects with a RangeError rather than returning the whole capture where one page was asked for. The decoded bitmap becomes the aspect reference, otherwise a width-only crop inherits the full document ratio and stretches. | 1 个月前 | |
test: pin devicePixelRatio in DPR-dependent tests Three tests asserted results that only hold at devicePixelRatio 1, so they failed on any Retina machine. They are not new: they fail the same way at the v2.23.2 tag. Nothing caught them because no workflow runs the suite; it only ever runs locally. - srcset freezing: pickSrcsetCandidate mirrors the browser and picks the smallest density >= devicePixelRatio, so at DPR 2 the 2x candidate wins and the assertions about the frozen candidate no longer hold. Pin the ratio, and add a case at DPR 2 so the dependency is explicit rather than a magic constant. - Both srcset cases sat outside the describe block, so its clearAllMocks never reached them and an unconsumed mockResolvedValueOnce leaked from one into the next. Reset the snapFetch queue per test. - iframe #449: the nested capture defaults to window.devicePixelRatio, so the bitmap is legitimately 600x400 on Retina. Pin dpr and also assert the aspect ratio, which catches the actual regression (height tracking scrollHeight instead of the viewport) at any ratio. The product behaviour was correct in all three cases; only the tests were environment-fragile. | 1 个月前 | |
fix(styles): reproduce visibility and content-visibility the way browsers do Two rendering-fidelity bugs in the same style snapshot, both verified against real Chromium pixels and then checked on Chromium, WebKit and Firefox. visibility: a hidden ancestor was flattened to opacity:0. visibility is inherited but a descendant can restore it with visibility:visible and still paint; opacity cannot be undone that way, so any such descendant was lost. Keep visibility as visibility. content-visibility:hidden leaked its whole subtree into captures. The pre-clone pass forced both auto and hidden to visible, which erased the value the snapshot guard looked for, so the guard added to stop the leak could never fire. Two unit tests hid this: one exercised inlineAllStyles directly on an element that still carried hidden, the other asserted that forcing hidden was correct. Both passed while the feature was dead end to end. What browsers actually do: the element paints its own box (background, border, padding) and its contents are skipped outright, and a descendant's visibility:visible does NOT bring them back. So neither half of the old code was right, and mapping it to visibility:hidden would erase the box too. Force only auto, which is what the offscreen-skipping issue needed, and carry the content-visibility declaration itself into the snapshot so the rasterizer applies the real semantics natively. Both contradicting tests are updated to the verified behaviour. | 1 个月前 | |
lint tests | 11 个月前 | |
Make the test suite pass on Firefox and WebKit The suite was Chromium-shaped in several places; no product code changes: - replace the corrupt 1x1 PNG fixtures (bad zlib checksum in IDAT): Chromium/WebKit decode them leniently, Firefox rejects with EncodingError - restore globalThis.Image after the Safari toImg branch test; the leaked stub resolved decode() before load, which happened to work on Chromium but produced 0-sized canvases (null blobs) on Firefox - gate engine-specific behavior: WebP encode falls back to PNG where unsupported, Safari keeps natural svg size for width/height options, Firefox swaps checkbox/radio for the visual replacement, WebKit/Firefox resolve srcset currentSrc eagerly (assert the freeze invariant instead) - accept Gecko serializations: vertical-align exposed via its longhand alignment-baseline in computed-style enumeration, uniform scale as '2', fractional scroll positions, ::-webkit-scrollbar declarations dropped from CSSOM Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
fix: keep Safari toSvg/toImg vector at scale instead of rasterizing to PNG | 1 个月前 | |
Make the test suite pass on Firefox and WebKit The suite was Chromium-shaped in several places; no product code changes: - replace the corrupt 1x1 PNG fixtures (bad zlib checksum in IDAT): Chromium/WebKit decode them leniently, Firefox rejects with EncodingError - restore globalThis.Image after the Safari toImg branch test; the leaked stub resolved decode() before load, which happened to work on Chromium but produced 0-sized canvases (null blobs) on Firefox - gate engine-specific behavior: WebP encode falls back to PNG where unsupported, Safari keeps natural svg size for width/height options, Firefox swaps checkbox/radio for the visual replacement, WebKit/Firefox resolve srcset currentSrc eagerly (assert the freeze invariant instead) - accept Gecko serializations: vertical-align exposed via its longhand alignment-baseline in computed-style enumeration, uniform scale as '2', fractional scroll positions, ::-webkit-scrollbar declarations dropped from CSSOM Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
fix: derive missing export dimension from post-bleed viewBox, not pre-bleed box toImg/toCanvas computed the omitted width/height from meta.w0/h0 (the element's pre-bleed content box). Under outerShadows:true an asymmetric shadow/blur/outline bleeds unevenly per side, so w0/h0's aspect ratio no longer matches the actual rasterized image (meta.vbW/vbH) and drawImage/img sizing stretched it non-uniformly. Prefer vbW/vbH when present, falling back to w0/h0 then natural size for callers that don't populate it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> | 1 个月前 | |
feat(plugins): expose capture geometry, exact export options and canvas cropping Document exporters need to know where the content sits inside the serialized SVG and to rasterize it a region at a time. Three additions, all read-only surface: - result.meta / context.meta: frozen render geometry (viewBox size, logical capture box, contentX/contentY origin, resolved clip window). contentX/Y are the exact origin rather than a centred guess, so they stay correct under asymmetric shadows, transformed roots and clip windows. The property is non-writable so a hook cannot desynchronise it from the canonical SVG, but it stays configurable: options is caller-owned and toPng/toJpg/toWebp pass their raw bag straight to captureDOM, so a reused object must be able to take a second capture's geometry instead of throwing "Cannot redefine property". - export.requestedOptions: a frozen shallow copy of exactly what the caller passed, snapshotted when toXxx() is called rather than when the export reaches the session queue. Callers commonly reuse an options object, and a slow earlier export must not let later mutation rewrite an already-requested one. Key presence is preserved, so a plugin can tell an explicit value from an omitted one even when it equals the capture default. - toCanvas({ crop }): windows the SVG in viewBox coordinates by rewriting the header before decode, so a long capture can be rasterized page by page instead of allocating one bitmap that exceeds the browser decode limits. The crop clips to the intersection with the viewBox and never degrades silently: an empty, non-finite or fully outside window, or a non-SVG payload, rejects with a RangeError rather than returning the whole capture where one page was asked for. The decoded bitmap becomes the aspect reference, otherwise a width-only crop inherits the full document ratio and stretches. | 1 个月前 | |
fix: derive missing export dimension from post-bleed viewBox, not pre-bleed box toImg/toCanvas computed the omitted width/height from meta.w0/h0 (the element's pre-bleed content box). Under outerShadows:true an asymmetric shadow/blur/outline bleeds unevenly per side, so w0/h0's aspect ratio no longer matches the actual rasterized image (meta.vbW/vbH) and drawImage/img sizing stretched it non-uniformly. Prefer vbW/vbH when present, falling back to w0/h0 then natural size for callers that don't populate it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> | 1 个月前 | |
Make the test suite pass on Firefox and WebKit The suite was Chromium-shaped in several places; no product code changes: - replace the corrupt 1x1 PNG fixtures (bad zlib checksum in IDAT): Chromium/WebKit decode them leniently, Firefox rejects with EncodingError - restore globalThis.Image after the Safari toImg branch test; the leaked stub resolved decode() before load, which happened to work on Chromium but produced 0-sized canvases (null blobs) on Firefox - gate engine-specific behavior: WebP encode falls back to PNG where unsupported, Safari keeps natural svg size for width/height options, Firefox swaps checkbox/radio for the visual replacement, WebKit/Firefox resolve srcset currentSrc eagerly (assert the freeze invariant instead) - accept Gecko serializations: vertical-align exposed via its longhand alignment-baseline in computed-style enumeration, uniform scale as '2', fractional scroll positions, ::-webkit-scrollbar declarations dropped from CSSOM Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
lint tests | 11 个月前 | |
update tests | 1 个月前 | |
fix(capture): stop background/asset passes losing the session nodeMap when same-origin iframes capture concurrently Sibling same-origin iframes rasterize in parallel, and each nested capture's applyCachePolicy reassigns the global cache.session.nodeMap. The save/restore in rasterizeIframe interleaves across siblings, so the global could end up pointing at an orphaned nested map. inlineBackgroundImages (and the other post-clone passes) resolved clone→source through that empty map, silently dropping every CSS background-image outside the iframes on affected runs. prepareClone now returns the session's clone→source nodeMap, captureDOM keeps that reference on its state, and all post-clone consumers receive it explicitly: neutralizeRootMarginCollapse, ligatureIconToImage, inlineBackgroundImages, emulateBackdropFilters, compressCloneAssets and reconcileCloneLayout. The module functions default to the global map at call time, so direct callers (preCache, unit tests) are unaffected. The rasterizeIframe save/restore stays as best-effort hygiene for the global, with its comment updated to say nothing may rely on it mid-capture. Regression test captures 3 stylesheet-URL backgrounds next to 3 asymmetric same-origin iframes under both fast:true and fast:false. Without the fix it reports 0 inlined backgrounds instead of 3. Closes #463 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
fix(capture): preserve mask-mode and mask-composite through shorthand inlining. Closes #402 | 4 个月前 | |
Make the test suite pass on Firefox and WebKit The suite was Chromium-shaped in several places; no product code changes: - replace the corrupt 1x1 PNG fixtures (bad zlib checksum in IDAT): Chromium/WebKit decode them leniently, Firefox rejects with EncodingError - restore globalThis.Image after the Safari toImg branch test; the leaked stub resolved decode() before load, which happened to work on Chromium but produced 0-sized canvases (null blobs) on Firefox - gate engine-specific behavior: WebP encode falls back to PNG where unsupported, Safari keeps natural svg size for width/height options, Firefox swaps checkbox/radio for the visual replacement, WebKit/Firefox resolve srcset currentSrc eagerly (assert the freeze invariant instead) - accept Gecko serializations: vertical-align exposed via its longhand alignment-baseline in computed-style enumeration, uniform scale as '2', fractional scroll positions, ::-webkit-scrollbar declarations dropped from CSSOM Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
feat: clip option + snapdom.viewport() — region capture with offscreen culling clip: 'viewport' | {x,y,width,height} captures a page region ~24x faster than full-page by pruning offscreen subtrees into layout-preserving husks during deepClone (no styling/inlining/raster work for culled content) and windowing the viewBox to the clip rect resolved once at cull time. - Culling is escape-aware: descendants painting into the window (fixed widgets, portal-less modals, negative offsets, transforms) veto the cull; rtl/vertical-writing overflow expands the test box; replaced inline elements (img/canvas/...) are cullable so offscreen fetches are skipped. - freezeViewportPositioned replaces freezeSticky everywhere: fixed/sticky clones re-anchor to their painted gBCR position and hoist to the clone root (escapes #364 scroll wrappers, overflow clipping and CB-forming ancestors); sticky leaves an in-flow placeholder; individual rotate/scale compose via DOMMatrix; shadow-scoped clones freeze in place. Also fixes stuck sticky/fixed inside scrolled element captures (d33: header/footer/left-sidebar under two-axis scroll + inner scroller state). - Chromium 148/149 workaround: an absolute box exactly on the window edge makes a later positioned sibling vanish in SVG-image rasterization; nudge 1px outside the window (clipped away) to break the coincidence. - Clip mode skips the clone-in-document measurement pass, scrollHeight expansion, forceContentVisibility, and prunes lineClamp/font-collection walks by window intersection; root bleed is never added to a clip window. - Non-clip hot path unchanged (within benchmark noise). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
fix: stop concurrent captures from sharing session maps and counter state | 1 个月前 | |
fix: key the embedded-fonts cache by document identity buildFontsCacheKey ignored which document the fonts were scanned from, so a same-origin iframe capture could collide with the parent document in cache.resource and inherit the wrong @font-face resource. Assign each Document instance an id via WeakMap and fold it into the key. | 1 个月前 | |
fix(fonts): keep a custom @font-face when the page asks for a far weight/stretch (#478) A family declared with a single font file fell back to a system font whenever the element asked for a weight or a stretch that face did not literally cover. embedCustomFonts filters faces against the variants the page actually uses, and two of those rules excluded rather than ranked: - weight was rejected past a ±300 distance, so one face at 400 used at 800 was dropped; - stretch required the request to fall inside the face's range, so a face with no font-stretch descriptor (100%) was dropped as soon as the page asked for 75%. With a multi-face family some sibling always survives the filter, which is why the reporter saw it only with "a custom single font file" and not with Roboto Condensed. With one face the only face is rejected, nothing is embedded, and the capture renders in a fallback. That contradicts CSS itself: the font matching algorithm never drops a family that has faces — it picks the closest one and synthesises the difference. The tight filter stays as the preferred path. A face it rejects is now held un-inlined and emitted only if its family ends the scan with no faces at all. A family that already has a good match therefore fetches nothing extra, so there is no payload or speed cost in the common case — the fallback only does work in the case that is currently broken. Tests: __tests__/module.fonts.issue478.test.js, 8 cases. Six reproduce the bug (heavier weight, lighter weight, narrower stretch, wider stretch, both at once, and the reporter's scenario) and fail without this change; two are controls that pass either way — a multi-face family still resolving normally, and a covered family still emitting exactly one face rather than dragging in its distant weights. Fixes #478 | 1 个月前 | |
fix(fonts): emit only the @font-face descriptors the source declared An absent descriptor is not the same as its default on a variable font: font-weight:400 pins the wght axis, omitting it leaves the axis free. Re-emitting the default flattened every axis in the capture. Defaults are still used for face matching. ref #479 | 1 个月前 | |
test: use importOriginal in vi.mock factories (fixes flaky WebKit runs) | 1 个月前 | |
test: use importOriginal in vi.mock factories (fixes flaky WebKit runs) | 1 个月前 | |
test: use importOriginal in vi.mock factories (fixes flaky WebKit runs) | 1 个月前 | |
fix: parse final font-face declarations without semicolons (#475) | 1 个月前 | |
update tests | 1 个月前 | |
fix: capture roots that are themselves the target element were skipped querySelectorAll never matches the element it's called on, so calling snapdom() directly on an <img>, <picture>, a Material icon span, a <canvas>, or a blob: image (no wrapper) silently skipped compress, icon-font ligature conversion, picture/lazy resolution, Safari canvas warmup, and blob URL resolution for that root — only descendants were ever checked. Same shape as #461. Adds a small selfAndDescendants / root.matches() check at each of those call sites. | 1 个月前 | |
fix(clamp): bake single-line text-overflow:ellipsis for Safari/Firefox Firefox and Safari don't honour `text-overflow: ellipsis` inside a <foreignObject>, so single-line truncated text captured without its ellipsis (and overflowed its box). Resolve it up front like the multi-line clamp: a new textEllipsis pass binary-searches the prefix that fits and bakes in the '…', for nowrap, overflow-clipped plain-text containers. Runs in the same subtree walk as lineClamp, sharing one getComputedStyle read per node so the capture hot path isn't regressed. Fixes #431 | 2 个月前 | |
fix: capture roots that are themselves the target element were skipped querySelectorAll never matches the element it's called on, so calling snapdom() directly on an <img>, <picture>, a Material icon span, a <canvas>, or a blob: image (no wrapper) silently skipped compress, icon-font ligature conversion, picture/lazy resolution, Safari canvas warmup, and blob URL resolution for that root — only descendants were ever checked. Same shape as #461. Adds a small selfAndDescendants / root.matches() check at each of those call sites. | 1 个月前 | |
fix: realm-safe element guards — pseudos and per-node handlers now survive iframe-content captures Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
fix: resolve image-set()/-webkit-image-set() by device pixel ratio Both background-image entries and pseudo-element content: image-set(...) used to fall through to a generic url() regex that always grabbed whichever candidate appeared first in the string — silently discarding resolution (e.g. always inlining the 1x image on a 2x display) or, for pseudo content, rendering the raw CSS source as literal text. Add resolveImageSetURL to pick the candidate matching the live dppx. | 1 个月前 | |
update tests | 1 个月前 | |
fix(styles): keep the height of wrappers whose children are all out of flow stripHeightForWrappers dropped height/block-size from any "transparent flow wrapper", relying on hasFlowFast to prove the element had in-flow content that would re-establish its box in the clone. Both of hasFlowFast's signals answer a different question than the one being asked: - `textContent` also sees text inside absolutely positioned descendants, so a wrapper holding only an abspos overlay with a caption reads as having text. - `scrollHeight` is floored by clientHeight, so a non-scrolling block reports its own used height no matter what its children are. The old comment claimed abspos children do not contribute; in practice the probe never drops to the padding it was compared against. A hero banner sized `height:100vh` whose children are all `position:absolute` therefore lost its height. Inside the foreignObject the authored stylesheet is gone, nothing restores it and the wrapper collapses to 0 — every later section shifts up by the banner's height and paints over it. hasFlowFast now asks the real question: direct non-blank text nodes, an immediate <br>, or an element child that is itself in flow. The children walk runs only after the cheap text/<br> paths miss, and skips display:none. `cs` is no longer needed, so it is dropped from the signature. Regression test covers the abspos-only wrapper end to end (size survives into the exported SVG) and via the clone style key, plus a display:none-only wrapper. A negative control keeps a genuinely transparent wrapper's height stripped, so the pass still does its job. Without the fix 3 of the 4 fail. Closes #467 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> | 1 个月前 | |
fix: stripe height | 1 个月前 | |
fix: stripe height | 1 个月前 | |
fix(styles): reproduce visibility and content-visibility the way browsers do Two rendering-fidelity bugs in the same style snapshot, both verified against real Chromium pixels and then checked on Chromium, WebKit and Firefox. visibility: a hidden ancestor was flattened to opacity:0. visibility is inherited but a descendant can restore it with visibility:visible and still paint; opacity cannot be undone that way, so any such descendant was lost. Keep visibility as visibility. content-visibility:hidden leaked its whole subtree into captures. The pre-clone pass forced both auto and hidden to visible, which erased the value the snapshot guard looked for, so the guard added to stop the leak could never fire. Two unit tests hid this: one exercised inlineAllStyles directly on an element that still carried hidden, the other asserted that forcing hidden was correct. Both passed while the feature was dead end to end. What browsers actually do: the element paints its own box (background, border, padding) and its contents are skipped outright, and a descendant's visibility:visible does NOT bring them back. So neither half of the old code was right, and mapping it to visibility:hidden would erase the box too. Force only auto, which is what the offscreen-skipping issue needed, and carry the content-visibility declaration itself into the snapshot so the rasterizer applies the real semantics natively. Both contradicting tests are updated to the verified behaviour. | 1 个月前 | |
update tests | 1 年前 | |
fix: collect svg defs referenced from the root svg's own attributes | 1 个月前 | |
fix(images): inline the capture root itself when it is an <img> or SVG <image> inlineImages collected candidates with clone.querySelectorAll, which only matches descendants. Capturing a bare <img> (or SVG <image>) element left the root's src as an external URL, and svg-as-image rasterization refuses external loads — the output came out blank regardless of image format. Closes #461 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> | 1 个月前 | |
feat(plugin)!: rename prompt-export to agent-map, focused on visual agents The previous prompt-export tried to be an "LLM-ready package for any UI inspection task" and didn't decisively beat simpler alternatives (raw screenshots, ax-tree dumps) on a real benchmark. Repositioning around a clearer use case the plugin actually wins at: Set-of-Mark prompting for visual agents — image with numbered badges + a compact JSON map from badge index to {role, name, bbox, state}. Output shape changes (BREAKING): Before: { image, elements: [{id, tag, type, name, text, bbox: {x,y,w,h}, attributes, state, styles, covered}], prompt, dimensions } After: { image, map: [{i, n, r, b: [x,y,w,h], s?}], dimensions } - `r` is an ARIA-style role derived from the element (button / link / checkbox / radio / textbox / combobox / slider / heading / …) - `b` is a 4-tuple instead of an object — agents iterate it directly - `s` (state) only appears when meaningful: checked / disabled / focus / expanded / pressed / selected / value / open / selectedText / covered - aria-expanded=false and aria-pressed=false are now both captured (the prior plugin filtered "false" from attributes, which silently dropped a meaningful "this toggle exists and is OFF" signal) - The pre-formatted prompt text and its options are gone — callers compose their own prompts from the map - Default selector excludes semantics; `semantic: true` to opt in - `image: 'annotated' | 'raw' | false` controls image output; `image: false` skips canvas + toDataURL entirely API surface: import { agentMap } from '@zumer/snapdom-plugins/agent-map' const result = await snapdom(el, { plugins: [agentMap()] }) const { image, map, dimensions } = await result.toAgentMap() Bench (4 fixtures, 9 questions): the new minimal map answers 8/9 (vs 6/9 for the prior prompt-export full output) at ~656 tokens. Combined with a low-detail screenshot it hits 9/9 (full coverage) at ~996 tokens — the only variant in the bench that does. Standalone screenshots reach 8/9 at ~340 tokens; ax-tree dumps reach 7/9 at ~298 tokens. The plugin's contribution over those baselines is the covered + aria state evidence and the index↔bbox table for click coordinates. Migration: `import { agentMap } from '@zumer/snapdom-plugins/agent-map'`, `result.toAgentMap()`. The old subpath `/prompt-export` is removed. Note: docs/plugins.html, docs/llms{,-full}.txt and the root README still reference promptExport in passing — those are doc-only updates deferred to a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> | 4 个月前 | |
Add new super power plugins | 3 个月前 | |
Add new super power plugins | 3 个月前 | |
feat(plugins): expose capture geometry, exact export options and canvas cropping Document exporters need to know where the content sits inside the serialized SVG and to rasterize it a region at a time. Three additions, all read-only surface: - result.meta / context.meta: frozen render geometry (viewBox size, logical capture box, contentX/contentY origin, resolved clip window). contentX/Y are the exact origin rather than a centred guess, so they stay correct under asymmetric shadows, transformed roots and clip windows. The property is non-writable so a hook cannot desynchronise it from the canonical SVG, but it stays configurable: options is caller-owned and toPng/toJpg/toWebp pass their raw bag straight to captureDOM, so a reused object must be able to take a second capture's geometry instead of throwing "Cannot redefine property". - export.requestedOptions: a frozen shallow copy of exactly what the caller passed, snapshotted when toXxx() is called rather than when the export reaches the session queue. Callers commonly reuse an options object, and a slow earlier export must not let later mutation rewrite an already-requested one. Key presence is preserved, so a plugin can tell an explicit value from an omitted one even when it equals the capture default. - toCanvas({ crop }): windows the SVG in viewBox coordinates by rewriting the header before decode, so a long capture can be rasterized page by page instead of allocating one bitmap that exceeds the browser decode limits. The crop clips to the intersection with the viewBox and never degrades silently: an empty, non-finite or fully outside window, or a non-SVG payload, rejects with a RangeError rather than returning the whole capture where one page was asked for. The decoded bitmap becomes the aspect reference, otherwise a width-only crop inherits the full document ratio and stretches. | 1 个月前 | |
feat(plugins): per-node resolveNode hook + internal tag handler registry - resolveNode(node, ctx): first plugin returning a value wins — Node replaces the clone (mapped to source, box styles applied), null skips, undefined continues. Hooks are collected once per capture; captures without them pay one falsy check per node. - IFRAME/CANVAS/VIDEO/AUDIO branches extracted into a tagHandlers registry (same logic, same precedence: iframe before data-capture=placeholder, the rest after). - Documented in PLUGIN_SPEC.md; tests for replace/skip/fall-through. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
Add new super power plugins | 3 个月前 | |
fix: repair the broken htmlInCanvas re-export in the plugins barrel index.js re-exported a name html-in-canvas.js never exported (htmlInCanvasPlugin, not htmlInCanvas), which throws at import time and takes down the whole combined-import form for all 11 plugins. Added a smoke test that imports the barrel directly. | 1 个月前 | |
fix: stop concurrent captures from sharing session maps and counter state | 1 个月前 | |
fix: freeze the image the browser actually shows (srcset descriptors, type filters, content:url) | 1 个月前 | |
fix(pseudo): pin white-space:nowrap on single-line hosts with multi-char pseudos SVG rasterized via <img> resolves ui-sans-serif/system-ui with wider metrics, wrapping inline pseudo text (e.g. ' Pro') in cases where the live DOM fits on one line. Pin nowrap on host + pseudo span (the span needs it explicitly to override its own snapshot's white-space:normal). Gate skips single-char and url() pseudos to avoid the rect read. | 4 个月前 | |
fix: empty pseudo-element box sized by CSS keeps its width (fixes #452) The pseudo span was keyed with getStyleKey defaults (sizedByContent=true, isFlexItem=false), so an empty ::before dot (content:'', width/height from CSS) went through width softening as if it were content-sized: inside a flex host it collapsed to 0 (the min-width floor lost to the flex item's computed min-width:auto) and a display:block dot stretched to the host width. Mirror the styles.js flags: an empty pseudo box keeps its width verbatim (#433 semantics) and a pseudo of a flex/grid host is a flex item (#406: no synthesized floor, min-width:auto normalized to 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
fix: stop concurrent captures from sharing session maps and counter state | 1 个月前 | |
test: split burst benchmark into isolated static/mutating files Each scenario now runs in its own file so vitest's per-file page isolation gives it a fresh module/cache/JIT baseline, instead of the mutating scenario silently inheriting warm state from the static one when both lived in the same file. | 1 个月前 | |
test: split burst benchmark into isolated static/mutating files Each scenario now runs in its own file so vitest's per-file page isolation gives it a fresh module/cache/JIT baseline, instead of the mutating scenario silently inheriting warm state from the static one when both lived in the same file. | 1 个月前 | |
lint tests | 11 个月前 | |
fix(export): flatten jpeg/webp background by resolved format, not export name normalizeExportOptions only injected the white background when the *export name* was 'jpeg'/'jpg'. But result.toBlob({type:'jpeg'}), result.toCanvas({format:'jpeg'}) and result.download({format:'jpeg'}) pass the image format in opts.format/opts.type while the export name is 'blob'/'canvas'/ 'download'. On a capture with no format (backgroundColor null) the transparent area was then encoded as black by JPEG. Resolve the real lossy format from the merged options and flatten white for jpeg/webp, matching createContext. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
bench: add published 2.16.0 (CDN) rows next to current src version Simple + complex size matrices and the image-gallery toPng (compress on/off) now compare src against the released 2.16.0, so branch runs show the delta in one pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
bench: add published 2.16.0 (CDN) rows next to current src version Simple + complex size matrices and the image-gallery toPng (compress on/off) now compare src against the released 2.16.0, so branch runs show the delta in one pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
lint tests | 11 个月前 | |
fix(#425): strip XML-invalid control chars before serializing the clone The ExtJS "EncodingError: The source image cannot be decoded" came from U+0003 (ETX) characters in the captured subtree — ExtJS uses them as a delimiter in hidden-input values. I already scrub control chars from attributes while cloning, but form fields re-apply the raw value afterwards via setAttribute('value', ...) and text nodes were never scrubbed, so the char survived into the serialized SVG and made the data: URL invalid XML 1.0 → img.decode() throws. I now sweep every attribute value and text/CDATA node of the finished clone in sanitizeCloneForXHTML (the single choke point right before serialization), stripping C0 controls except TAB/LF/CR plus U+FFFE/U+FFFF. Verified against the live repro: toCanvas now succeeds (910x614, zero invalid chars). | 3 个月前 | |
fix: keep an author-specified width on blockified spans (Ref #484) Width softening drops the used width of content-bearing inline-sized tags and re-adds it as a min-width floor, so a wider raster font can grow the box instead of re-wrapping its text. That substitution only reproduces the box when width is auto and the box is shrink-to-fit. Two shapes break it: - a blockified box in normal flow (span { display: block; width: 16px }): auto stretches it to the container and the floor cannot cap it, so the span rendered at 100% of its parent; - a flex/grid item, which gets no floor at all (#406) and collapsed to the width of its own text. softenNeedsAutoWidth() flags those two shapes (real inline boxes and the table box tree stay as they were, #429/#434), and hasSpecifiedWidth() then keeps the width verbatim when the author set one. getComputedStyle().width cannot answer that question because it resolves to the used width; Typed OM reports the computed value, where auto stays auto. Firefox has no Typed OM, so there the answer is inferred from layout. Intrinsic keywords (max-content, fit-content, stretch) count as content sizing and keep softening. | 24 天前 | |
fix: measure text truncation without replacing live text nodes (Ref #485) snapdom bakes `text-overflow: ellipsis` and `-webkit-line-clamp` by measuring the live element: it writes a candidate string, reads scrollWidth/scrollHeight and binary-searches the cut point. Writing that through `el.textContent` drops every child text node and inserts a fresh one, so a virtual-DOM framework holding the original node (React fiber, Vue vnode, Svelte block) fails on its next update with `NotFoundError: Failed to execute 'removeChild' on 'Node'` — long after the capture returned, which makes it very hard to trace back. The measurement now writes `textNode.data`, which mutates the node in place and keeps its identity (what React itself does for a single-text-child update). Elements reaching this path have no element children, so writing the whole string into the first text node and blanking the rest lays out exactly like a textContent write, and the undo puts every original chunk back where it was. | 22 天前 | |
fix: bound the canvas frame wait, warn on an empty canvas (Ref #486) Canvas readback rides on requestAnimationFrame: a WebGL/WebGPU canvas with preserveDrawingBuffer:false clears its drawing buffer as soon as the frame composites, so toDataURL has to run inside the frame (#480). rAF only fires while the document is being rendered, so in a background tab, a minimized window or an occluded view that await never resolved and the capture promise hung forever. nextFrame() keeps the same in-frame readback when rAF runs, skips the wait when the document is hidden (nothing composites there anyway) and falls back to a timeout otherwise. The reported case is the other half of the same subject: the capture ran in the same tick that created the animation, before the first frame had been drawn, so snapdom serialized an empty canvas — a valid PNG with nothing in it. Under `{ debug: true }` that now says so instead of looking like a silent failure. | 22 天前 | |
fix: let <use> icons inherit fill from the use site (Ref #487) A <use> instance takes its paint from the <use> element, so `<svg fill="red"><use href="#icon">` paints the referenced <symbol> red — both for a plain fill and for `currentColor`. snapdom emits a per-tag "base reset" built from `all: initial` for every tag it captures, and <symbol> was not in the list of tags that must not generate one: the resulting `symbol { fill: rgb(0,0,0); color: rgb(0,0,0); … }` rule matched the referenced element, and a declaration on the instance beats anything inherited from the <use> site, so every sprite icon rasterized black. <symbol> is a template that is never painted on its own, like <defs>, <g>, <use>, <pattern> or <marker>, all of which were already exempt. It now generates no base rule and no class, and the instance inherits from the <use> element again. A fill declared inside the symbol still wins, and <g>/<defs> targets are unchanged. | 22 天前 | |
Fix offscreen captures. Ref #488 | 17 天前 | |
Fix transformed reconciliation. Ref #489 | 17 天前 | |
fix: fractional with. Ref #491 | 13 天前 | |
fix: honor the localFonts option in the capture path localFonts was accepted and documented but only wired into preCache — the capture pipeline never forwarded it to embedCustomFonts, so snapdom(el, { localFonts: [...] }) embedded nothing. The capture now passes it through, so a user-provided font file is embedded as @font-face and the rasterized SVG uses it instead of falling back. Useful when content relies on a font the SVG-as-image path can't resolve (e.g. system-ui): supply a metric-compatible file via localFonts and reference its family in the content. | 3 个月前 | |
fix: root margin-collapse clipping (#426) and oversized-raster decode failure (#425) #426: margins that collapse through the captured root's top/bottom edge land outside its border box. The clone, isolated in an all:initial wrapper, no longer collapsed them, pushing content inward and clipping the opposite edge. neutralizeRootMarginCollapse() walks the collapse-through chain and zeroes those leading/trailing margins on the clone so content sits flush with the captured border box (respects padding/border/BFC; runs once per capture on the root only). #425: snapdom imposed no upper bound on output size, so a large element x scale x dpr (dpr auto-doubles on Retina) produced an SVG/canvas past the browser's 16384px-per-side / ~268M-px limits, throwing an opaque async "EncodingError: The source image cannot be decoded" in img.decode(). toCanvas now clamps the SVG before decode (viewBox preserved -> content just downscales) and the device canvas (incl. dpr), warning with the knobs to adjust instead of failing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
lint tests | 11 个月前 | |
refactor(tests): simplify tests for *-prefixed HTML attributes | 2 个月前 | |
fix: emit captured width as a min-width floor instead of dropping it Table cells and inline-sized boxes keep their size but can still grow to fit a wider raster font (no wrap, no collapse). Replaced elements keep their explicit width. Closes #433, #434, #436 | 2 个月前 | |
Make the test suite pass on Firefox and WebKit The suite was Chromium-shaped in several places; no product code changes: - replace the corrupt 1x1 PNG fixtures (bad zlib checksum in IDAT): Chromium/WebKit decode them leniently, Firefox rejects with EncodingError - restore globalThis.Image after the Safari toImg branch test; the leaked stub resolved decode() before load, which happened to work on Chromium but produced 0-sized canvases (null blobs) on Firefox - gate engine-specific behavior: WebP encode falls back to PNG where unsupported, Safari keeps natural svg size for width/height options, Firefox swaps checkbox/radio for the visual replacement, WebKit/Firefox resolve srcset currentSrc eagerly (assert the freeze invariant instead) - accept Gecko serializations: vertical-align exposed via its longhand alignment-baseline in computed-style enumeration, uniform scale as '2', fractional scroll positions, ::-webkit-scrollbar declarations dropped from CSSOM Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
Feature enable tree-shakeable code | 9 个月前 | |
lint tests | 11 个月前 | |
Make the test suite pass on Firefox and WebKit The suite was Chromium-shaped in several places; no product code changes: - replace the corrupt 1x1 PNG fixtures (bad zlib checksum in IDAT): Chromium/WebKit decode them leniently, Firefox rejects with EncodingError - restore globalThis.Image after the Safari toImg branch test; the leaked stub resolved decode() before load, which happened to work on Chromium but produced 0-sized canvases (null blobs) on Firefox - gate engine-specific behavior: WebP encode falls back to PNG where unsupported, Safari keeps natural svg size for width/height options, Firefox swaps checkbox/radio for the visual replacement, WebKit/Firefox resolve srcset currentSrc eagerly (assert the freeze invariant instead) - accept Gecko serializations: vertical-align exposed via its longhand alignment-baseline in computed-style enumeration, uniform scale as '2', fractional scroll positions, ::-webkit-scrollbar declarations dropped from CSSOM Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
Make the test suite pass on Firefox and WebKit The suite was Chromium-shaped in several places; no product code changes: - replace the corrupt 1x1 PNG fixtures (bad zlib checksum in IDAT): Chromium/WebKit decode them leniently, Firefox rejects with EncodingError - restore globalThis.Image after the Safari toImg branch test; the leaked stub resolved decode() before load, which happened to work on Chromium but produced 0-sized canvases (null blobs) on Firefox - gate engine-specific behavior: WebP encode falls back to PNG where unsupported, Safari keeps natural svg size for width/height options, Firefox swaps checkbox/radio for the visual replacement, WebKit/Firefox resolve srcset currentSrc eagerly (assert the freeze invariant instead) - accept Gecko serializations: vertical-align exposed via its longhand alignment-baseline in computed-style enumeration, uniform scale as '2', fractional scroll positions, ::-webkit-scrollbar declarations dropped from CSSOM Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
update tests | 1 个月前 | |
lint tests | 11 个月前 | |
fix(capture): honor filterMode:'remove' in shrink pass and height estimate filterMode:'remove' dropped children from the clone but the bbox shrink-pass and estimateKeptHeight only checked excludeMode, so the capture size never collapsed to match the filtered layout. Fixes #458 | 1 个月前 | |
test: pin devicePixelRatio in DPR-dependent tests Three tests asserted results that only hold at devicePixelRatio 1, so they failed on any Retina machine. They are not new: they fail the same way at the v2.23.2 tag. Nothing caught them because no workflow runs the suite; it only ever runs locally. - srcset freezing: pickSrcsetCandidate mirrors the browser and picks the smallest density >= devicePixelRatio, so at DPR 2 the 2x candidate wins and the assertions about the frozen candidate no longer hold. Pin the ratio, and add a case at DPR 2 so the dependency is explicit rather than a magic constant. - Both srcset cases sat outside the describe block, so its clearAllMocks never reached them and an unconsumed mockResolvedValueOnce leaked from one into the next. Reset the snapFetch queue per test. - iframe #449: the nested capture defaults to window.devicePixelRatio, so the bitmap is legitimately 600x400 on Retina. Pin dpr and also assert the aspect ratio, which catches the actual regression (height tracking scrollHeight instead of the viewport) at any ratio. The product behaviour was correct in all three cases; only the tests were environment-fragile. | 1 个月前 | |
fix(css): emit the base reset for tags evicted from the defaultStyle cache generateDedupedBaseCSS built the per-tag base reset by reading cache.defaultStyle directly. That cache is an EvictingMap capped at MAX_DEFAULT_STYLE (30), and the reset is generated at the very end of a capture — so any document using more than 30 distinct tags had its earliest-registered tags already evicted. The lookup returned undefined and `continue` silently emitted no reset rule for them. The base reset is what neutralizes the UA stylesheet inside the foreignObject. Without it those tags fall back to UA defaults, so properties the page had reset to the CSS initial value (and which are therefore diffed out of the element's generated class) reappear: h1..h3/p margins, hr borders, list padding. The capture then reflows taller than the source, shifting every later section down. Which tags are affected depends on registration order, so the corruption is silent and varies from page to page. Observed on a real site: 39 distinct tags, and the tags at first-appearance ranks 15-23 (picture, main, h1, h2, h3, strong, p, hr) all lost their reset while h4 at rank 25 survived. Four <h3>s regained the UA 1em margin-block-start, growing the article by 121px and pushing a button 171px down over the section below it. Resolve through getDefaultStyleForTag instead: it is memoized, idempotent, and re-derives an evicted entry on demand. NO_DEFAULTS_TAGS still yields {} and is dropped by the existing empty-key guard, so SVG/head tags are unaffected. Tests cover both the end-to-end path (a >30-tag capture keeps h3 in the reset) and the unit invariant (every used tag with defaults appears in the output, even from a cleared cache). Both fail without the fix. Note for reviewers: the fillers in the end-to-end test must have distinct UA styling. Tags that compute identically (aside/section/nav/header/…) collide on the style-key memo in styles.js, never call getDefaultStyleForTag, and so never occupy a cache slot — with those the capture registers only 28 tags, stays under the cap and the bug does not trigger. Closes #468 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> | 1 个月前 | |
fix(capture): drop invisible border props from style snapshot. See #390 | 4 个月前 | |
fix: fractional with. Ref #491 | 13 天前 | |
update tests | 1 个月前 | |
fix(background): resolve relative URLs and fallback to `background` shorthand for url() when background-image is empty. Closes #343 | 5 个月前 | |
fix: resolve image-set()/-webkit-image-set() by device pixel ratio Both background-image entries and pseudo-element content: image-set(...) used to fall through to a generic url() regex that always grabbed whichever candidate appeared first in the string — silently discarding resolution (e.g. always inlining the 1x image on a 2x display) or, for pseudo content, rendering the raw CSS source as literal text. Add resolveImageSetURL to pick the candidate matching the live dppx. | 1 个月前 | |
fix(styles): reproduce visibility and content-visibility the way browsers do Two rendering-fidelity bugs in the same style snapshot, both verified against real Chromium pixels and then checked on Chromium, WebKit and Firefox. visibility: a hidden ancestor was flattened to opacity:0. visibility is inherited but a descendant can restore it with visibility:visible and still paint; opacity cannot be undone that way, so any such descendant was lost. Keep visibility as visibility. content-visibility:hidden leaked its whole subtree into captures. The pre-clone pass forced both auto and hidden to visible, which erased the value the snapshot guard looked for, so the guard added to stop the leak could never fire. Two unit tests hid this: one exercised inlineAllStyles directly on an element that still carried hidden, the other asserted that forcing hidden was correct. Both passed while the feature was dead end to end. What browsers actually do: the element paints its own box (background, border, padding) and its contents are skipped outright, and a descendant's visibility:visible does NOT bring them back. So neither half of the old code was right, and mapping it to visibility:hidden would erase the box too. Force only auto, which is what the offscreen-skipping issue needed, and carry the content-visibility declaration itself into the snapshot so the rasterizer applies the real semantics natively. Both contradicting tests are updated to the verified behaviour. | 1 个月前 | |
fix: shadow fidelity in WebKit's SVG rasterization WebKit renders box-/text-shadow with the Y offset flipped at the svg's natural size, and corrupts blurred shadows entirely at any other rasterization scale. Probe both facts once per session, pre-negate the Y offsets, and rasterize shadowed svgs 1:1 before resampling. Also: the old drop-shadow fallback now halves the blur (WebKit reads the radius as sigma), text-shadow finally contributes root bleed on every engine, and the WebKit-only draft prop stroke-color is excluded from capture — when re-stated it silently disables text-shadow painting. | 1 个月前 | |
Split visual demo suite into 6 shards and gate network-bound demos Vitest parallelises by file, so the single visual.demos.test.js was the whole wall clock of `npm test`: one worker ran every demo in series while the rest of the suite finished around it. The file becomes a shared module plus six round-robin shards, which needs @zumer/snapdiff >= 0.3.0 (0.2.x clears artifacts per file, so shard N wiped shard N-1's report). Demos that pull KaTeX/MathJax/webfonts from a CDN were timing out on whichever demo happened to be waiting on the network, never reproducibly. Added a generic network gate: a broker in the node process measures throughput once per run (not latency: a 25KB stylesheet in 627ms while 86KB of woff2 took 6.6s) and answers parallel, serial or skip. Browser workers consult it through a single shared lane; the mode only ratchets stricter as the run congests the link. Override with NETWORK_GATE. Also adds `npm run test:visual` and commits package-lock.json. | 13 天前 | |
Split visual demo suite into 6 shards and gate network-bound demos Vitest parallelises by file, so the single visual.demos.test.js was the whole wall clock of `npm test`: one worker ran every demo in series while the rest of the suite finished around it. The file becomes a shared module plus six round-robin shards, which needs @zumer/snapdiff >= 0.3.0 (0.2.x clears artifacts per file, so shard N wiped shard N-1's report). Demos that pull KaTeX/MathJax/webfonts from a CDN were timing out on whichever demo happened to be waiting on the network, never reproducibly. Added a generic network gate: a broker in the node process measures throughput once per run (not latency: a 25KB stylesheet in 627ms while 86KB of woff2 took 6.6s) and answers parallel, serial or skip. Browser workers consult it through a single shared lane; the mode only ratchets stricter as the run congests the link. Override with NETWORK_GATE. Also adds `npm run test:visual` and commits package-lock.json. | 13 天前 | |
Split visual demo suite into 6 shards and gate network-bound demos Vitest parallelises by file, so the single visual.demos.test.js was the whole wall clock of `npm test`: one worker ran every demo in series while the rest of the suite finished around it. The file becomes a shared module plus six round-robin shards, which needs @zumer/snapdiff >= 0.3.0 (0.2.x clears artifacts per file, so shard N wiped shard N-1's report). Demos that pull KaTeX/MathJax/webfonts from a CDN were timing out on whichever demo happened to be waiting on the network, never reproducibly. Added a generic network gate: a broker in the node process measures throughput once per run (not latency: a 25KB stylesheet in 627ms while 86KB of woff2 took 6.6s) and answers parallel, serial or skip. Browser workers consult it through a single shared lane; the mode only ratchets stricter as the run congests the link. Override with NETWORK_GATE. Also adds `npm run test:visual` and commits package-lock.json. | 13 天前 | |
Split visual demo suite into 6 shards and gate network-bound demos Vitest parallelises by file, so the single visual.demos.test.js was the whole wall clock of `npm test`: one worker ran every demo in series while the rest of the suite finished around it. The file becomes a shared module plus six round-robin shards, which needs @zumer/snapdiff >= 0.3.0 (0.2.x clears artifacts per file, so shard N wiped shard N-1's report). Demos that pull KaTeX/MathJax/webfonts from a CDN were timing out on whichever demo happened to be waiting on the network, never reproducibly. Added a generic network gate: a broker in the node process measures throughput once per run (not latency: a 25KB stylesheet in 627ms while 86KB of woff2 took 6.6s) and answers parallel, serial or skip. Browser workers consult it through a single shared lane; the mode only ratchets stricter as the run congests the link. Override with NETWORK_GATE. Also adds `npm run test:visual` and commits package-lock.json. | 13 天前 | |
Split visual demo suite into 6 shards and gate network-bound demos Vitest parallelises by file, so the single visual.demos.test.js was the whole wall clock of `npm test`: one worker ran every demo in series while the rest of the suite finished around it. The file becomes a shared module plus six round-robin shards, which needs @zumer/snapdiff >= 0.3.0 (0.2.x clears artifacts per file, so shard N wiped shard N-1's report). Demos that pull KaTeX/MathJax/webfonts from a CDN were timing out on whichever demo happened to be waiting on the network, never reproducibly. Added a generic network gate: a broker in the node process measures throughput once per run (not latency: a 25KB stylesheet in 627ms while 86KB of woff2 took 6.6s) and answers parallel, serial or skip. Browser workers consult it through a single shared lane; the mode only ratchets stricter as the run congests the link. Override with NETWORK_GATE. Also adds `npm run test:visual` and commits package-lock.json. | 13 天前 | |
Split visual demo suite into 6 shards and gate network-bound demos Vitest parallelises by file, so the single visual.demos.test.js was the whole wall clock of `npm test`: one worker ran every demo in series while the rest of the suite finished around it. The file becomes a shared module plus six round-robin shards, which needs @zumer/snapdiff >= 0.3.0 (0.2.x clears artifacts per file, so shard N wiped shard N-1's report). Demos that pull KaTeX/MathJax/webfonts from a CDN were timing out on whichever demo happened to be waiting on the network, never reproducibly. Added a generic network gate: a broker in the node process measures throughput once per run (not latency: a 25KB stylesheet in 627ms while 86KB of woff2 took 6.6s) and answers parallel, serial or skip. Browser workers consult it through a single shared lane; the mode only ratchets stricter as the run congests the link. Override with NETWORK_GATE. Also adds `npm run test:visual` and commits package-lock.json. | 13 天前 | |
exposed internal visual demos for SnapDOM testing and add new tests | 10 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 个月前 | ||
| 10 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 10 天前 | ||
| 10 天前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 17 天前 | ||
| 1 个月前 | ||
| 11 个月前 | ||
| 4 个月前 | ||
| 28 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 17 天前 | ||
| 13 天前 | ||
| 1 个月前 | ||
| 5 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 11 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 11 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 4 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 年前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 4 个月前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 4 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 11 个月前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 11 个月前 | ||
| 3 个月前 | ||
| 24 天前 | ||
| 22 天前 | ||
| 22 天前 | ||
| 22 天前 | ||
| 17 天前 | ||
| 17 天前 | ||
| 13 天前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 11 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 9 个月前 | ||
| 11 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 11 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 4 个月前 | ||
| 13 天前 | ||
| 1 个月前 | ||
| 5 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 13 天前 | ||
| 13 天前 | ||
| 13 天前 | ||
| 13 天前 | ||
| 13 天前 | ||
| 13 天前 | ||
| 10 天前 |