// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // State visualizer snapshot tests. // // Snapshots are stored as .html files in `test/state-viz-cases/`. // To (re)generate snapshots: // node --test --test-update-snapshots test/stateViz.js // @ts-check import { JSDOM } from "jsdom"; import fs from "node:fs"; import path from "node:path"; import { afterEach, beforeEach, test } from "node:test"; import { fileURLToPath } from "node:url"; import prettier from "prettier"; import { createStatePanel, updateStatePanelFromColumns, } from "../dist/ux/circuit-vis/state-viz/stateViz.js"; import { prepareStateVizColumnsFromAmpMap } from "../dist/ux/circuit-vis/state-viz/worker/stateVizPrep.js"; const documentTemplate = ` `; /** @type {JSDOM | null} */ let jsdom = null; beforeEach(() => { jsdom = new JSDOM(documentTemplate, { pretendToBeVisual: true, }); // Override the globals used by product code // @ts-expect-error - the `jsdom` typings and DOM typings don't match globalThis.window = jsdom.window; globalThis.document = jsdom.window.document; globalThis.Node = jsdom.window.Node; globalThis.HTMLElement = jsdom.window.HTMLElement; globalThis.SVGElement = jsdom.window.SVGElement; globalThis.SVGSVGElement = jsdom.window.SVGSVGElement; globalThis.XMLSerializer = jsdom.window.XMLSerializer; globalThis.getComputedStyle = jsdom.window.getComputedStyle.bind( jsdom.window, ); globalThis.requestAnimationFrame = jsdom.window.requestAnimationFrame.bind( jsdom.window, ); }); afterEach(() => { jsdom?.window.close(); jsdom = null; }); function getCasesDirectory() { return path.join( path.dirname(fileURLToPath(import.meta.url)), "state-viz-cases", ); } /** * @param {string} name */ function htmlSnapshotPath(name) { // Keep snapshots stable across OSes and paths. const safe = name.replace(/[^a-zA-Z0-9_.-]+/g, "_"); return path.join(getCasesDirectory(), safe + ".snapshot.html"); } /** * Check the current document against the stored snapshot. * @param {import("node:test").TestContext} t * @param {string} name */ async function checkDocumentSnapshot(t, name) { const rawHtml = new XMLSerializer().serializeToString(document) + "\n"; const formattedHtml = await prettier.format(rawHtml, { parser: "html", printWidth: 80, tabWidth: 2, useTabs: false, }); t.assert.fileSnapshot(formattedHtml, htmlSnapshotPath(name), { serializers: [(s) => String(s)], }); } /** * Creates a state panel, attaches it to the DOM, renders an amp map, * and returns the panel. * * @param {Record} ampMap * @param {number} [maxColumns] * @param {number} [minProbThreshold] */ function renderStatePanel(ampMap, maxColumns = 16, minProbThreshold = 0) { const panel = createStatePanel(); document.body.appendChild(panel); // For deterministic snapshots: disable animations. panel.style.setProperty("--stateAnimMs", "0ms"); // Ensure panel is expanded so its contents appear in snapshots. panel.classList.remove("collapsed"); const edge = panel.querySelector(".state-edge"); edge?.setAttribute("aria-expanded", "true"); const columns = prepareStateVizColumnsFromAmpMap(ampMap, { maxColumns: maxColumns, minProbThreshold: minProbThreshold, }); updateStatePanelFromColumns(panel, columns, { maxColumns: maxColumns }); return panel; } test("state viz snapshot - single basis state", async (t) => { renderStatePanel({ 0: { re: 1, im: 0 }, }); await checkDocumentSnapshot(t, t.name); }); test("state viz snapshot - superposition with phase", async (t) => { const invSqrt2 = Math.SQRT1_2; renderStatePanel({ 0: { re: invSqrt2, im: 0 }, 1: { re: 0, im: invSqrt2 }, // phase +π/2 }); await checkDocumentSnapshot(t, t.name); }); test("state viz snapshot - threshold aggregates to Others", async (t) => { renderStatePanel( { "00": { re: 0.94, im: 0 }, "01": { re: 0.2, im: 0 }, 10: { re: 0.1, im: 0 }, 11: { re: 0.05, im: 0 }, }, 8, 0.05, ); await checkDocumentSnapshot(t, t.name); }); // Ensure the cases directory exists when running tests in fresh environments. if (!fs.existsSync(getCasesDirectory())) { fs.mkdirSync(getCasesDirectory(), { recursive: true }); }