// 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(
``,
'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);
}
});
});