// Copyright (c) 2025-2026, RTE (https://www.rte-france.com) // This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. // If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. // SPDX-License-Identifier: MPL-2.0 import { describe, it, expect } from 'vitest'; import { buildSnapshotSvg, collectHighlightCss, serializeStrippedSvg, composeSnapshotMarkup, } from './bitmapSnapshot'; const makeSvg = (inner: string): SVGSVGElement => { const doc = new DOMParser().parseFromString( `${inner}`, 'image/svg+xml', ); return doc.documentElement as unknown as SVGSVGElement; }; describe('buildSnapshotSvg', () => { it('strips foreignObjects (canvas taint) but keeps the highlight clones', () => { const svg = makeSvg( '
L
' + '', ); const clone = buildSnapshotSvg(svg, { baseVb: { x: 1, y: 2, w: 30, h: 40 }, width: 800, height: 600, zoomTier: 'detail', }); expect(clone.querySelectorAll('foreignObject').length).toBe(0); expect(clone.querySelector('path.nad-overloaded')).toBeTruthy(); expect(clone.getAttribute('viewBox')).toBe('1 2 30 40'); expect(clone.getAttribute('width')).toBe('800'); expect(clone.getAttribute('height')).toBe('600'); expect(clone.getAttribute('data-zoom-tier')).toBe('detail'); // the live SVG must be left untouched expect(svg.querySelectorAll('foreignObject').length).toBe(1); }); it('injects the provided css as a leading '); expect(out).toContain('class="nad-overloaded"'); // body preserved // re-compose to a different viewBox reuses the same body (cache reuse) const out2 = composeSnapshotMarkup(serialized, { baseVb: { x: 9, y: 9, w: 9, h: 9 }, width: 10, height: 10 }); expect(out2).toContain('viewBox="9 9 9 9"'); expect(out2).toContain('class="nad-overloaded"'); }); it('re-declares the live --nad-halo-w on the snapshot root so var()-bound halo widths resolve', () => { const serialized = '' + ''; // With a live halo width, the inlined style sets --nad-halo-w on `svg` // (custom properties inherit to the cloned halos) ahead of the rules. const out = composeSnapshotMarkup(serialized, { baseVb: { x: 0, y: 0, w: 1, h: 1 }, width: 10, height: 10, haloWidthPx: 120, css: '.nad-overloaded path{stroke-width:var(--nad-halo-w,24px)}', }); expect(out).toContain('svg{--nad-halo-w:120px}'); expect(out.indexOf('--nad-halo-w:120px')).toBeLessThan(out.indexOf('.nad-overloaded path')); // No live width → no var declaration (falls back to the 24px default). const out2 = composeSnapshotMarkup(serialized, { baseVb: { x: 0, y: 0, w: 1, h: 1 }, width: 10, height: 10, css: '.nad-overloaded path{stroke-width:var(--nad-halo-w,24px)}', }); expect(out2).not.toContain('--nad-halo-w:'); }); }); describe('collectHighlightCss', () => { it('collects only the highlight/delta/edge-info rules from the stylesheets', () => { const style = document.createElement('style'); style.textContent = '.nad-overloaded path { stroke: orange; }' + '.some-unrelated-class { color: green; }' + '.nad-delta-positive polyline { stroke: blue; }'; document.head.appendChild(style); try { const css = collectHighlightCss(document); expect(css).toContain('nad-overloaded'); expect(css).toContain('nad-delta-positive'); expect(css).not.toContain('some-unrelated-class'); // Base non-scaling-stroke must be re-asserted or branch/delta lines // render sub-pixel (user-space) in the detached clone. expect(css).toContain('vector-effect:non-scaling-stroke'); } finally { document.head.removeChild(style); } }); });