microsoft/qdk
Publicmirrored from https://github.com/microsoft/qdkAvailable
source/npm/qsharp/test/stateViz.js
159lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT license. |
| 3 | |
| 4 | // State visualizer snapshot tests. |
| 5 | // |
| 6 | // Snapshots are stored as .html files in `test/state-viz-cases/`. |
| 7 | // To (re)generate snapshots: |
| 8 | // node --test --test-update-snapshots test/stateViz.js |
| 9 | |
| 10 | // @ts-check |
| 11 | |
| 12 | import { JSDOM } from "jsdom"; |
| 13 | import fs from "node:fs"; |
| 14 | import path from "node:path"; |
| 15 | import { afterEach, beforeEach, test } from "node:test"; |
| 16 | import { fileURLToPath } from "node:url"; |
| 17 | import prettier from "prettier"; |
| 18 | import { |
| 19 | createStatePanel, |
| 20 | updateStatePanelFromColumns, |
| 21 | } from "../dist/ux/circuit-vis/state-viz/stateViz.js"; |
| 22 | import { prepareStateVizColumnsFromAmpMap } from "../dist/ux/circuit-vis/state-viz/worker/stateVizPrep.js"; |
| 23 | |
| 24 | const documentTemplate = `<!doctype html><html> |
| 25 | <head> |
| 26 | <link rel="stylesheet" href="../../ux/qsharp-ux.css"> |
| 27 | <link rel="stylesheet" href="../../ux/qsharp-circuit.css"> |
| 28 | </head> |
| 29 | <body></body> |
| 30 | </html>`; |
| 31 | |
| 32 | /** @type {JSDOM | null} */ |
| 33 | let jsdom = null; |
| 34 | |
| 35 | beforeEach(() => { |
| 36 | jsdom = new JSDOM(documentTemplate, { |
| 37 | pretendToBeVisual: true, |
| 38 | }); |
| 39 | |
| 40 | // Override the globals used by product code |
| 41 | // @ts-expect-error - the `jsdom` typings and DOM typings don't match |
| 42 | globalThis.window = jsdom.window; |
| 43 | globalThis.document = jsdom.window.document; |
| 44 | globalThis.Node = jsdom.window.Node; |
| 45 | globalThis.HTMLElement = jsdom.window.HTMLElement; |
| 46 | globalThis.SVGElement = jsdom.window.SVGElement; |
| 47 | globalThis.SVGSVGElement = jsdom.window.SVGSVGElement; |
| 48 | globalThis.XMLSerializer = jsdom.window.XMLSerializer; |
| 49 | globalThis.getComputedStyle = jsdom.window.getComputedStyle.bind( |
| 50 | jsdom.window, |
| 51 | ); |
| 52 | globalThis.requestAnimationFrame = jsdom.window.requestAnimationFrame.bind( |
| 53 | jsdom.window, |
| 54 | ); |
| 55 | }); |
| 56 | |
| 57 | afterEach(() => { |
| 58 | jsdom?.window.close(); |
| 59 | jsdom = null; |
| 60 | }); |
| 61 | |
| 62 | function getCasesDirectory() { |
| 63 | return path.join( |
| 64 | path.dirname(fileURLToPath(import.meta.url)), |
| 65 | "state-viz-cases", |
| 66 | ); |
| 67 | } |
| 68 | |
| 69 | /** |
| 70 | * @param {string} name |
| 71 | */ |
| 72 | function htmlSnapshotPath(name) { |
| 73 | // Keep snapshots stable across OSes and paths. |
| 74 | const safe = name.replace(/[^a-zA-Z0-9_.-]+/g, "_"); |
| 75 | return path.join(getCasesDirectory(), safe + ".snapshot.html"); |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Check the current document against the stored snapshot. |
| 80 | * @param {import("node:test").TestContext} t |
| 81 | * @param {string} name |
| 82 | */ |
| 83 | async function checkDocumentSnapshot(t, name) { |
| 84 | const rawHtml = new XMLSerializer().serializeToString(document) + "\n"; |
| 85 | |
| 86 | const formattedHtml = await prettier.format(rawHtml, { |
| 87 | parser: "html", |
| 88 | printWidth: 80, |
| 89 | tabWidth: 2, |
| 90 | useTabs: false, |
| 91 | }); |
| 92 | |
| 93 | t.assert.fileSnapshot(formattedHtml, htmlSnapshotPath(name), { |
| 94 | serializers: [(s) => String(s)], |
| 95 | }); |
| 96 | } |
| 97 | |
| 98 | /** |
| 99 | * Creates a state panel, attaches it to the DOM, renders an amp map, |
| 100 | * and returns the panel. |
| 101 | * |
| 102 | * @param {Record<string, {re:number, im:number}>} ampMap |
| 103 | * @param {number} [maxColumns] |
| 104 | * @param {number} [minProbThreshold] |
| 105 | */ |
| 106 | function renderStatePanel(ampMap, maxColumns = 16, minProbThreshold = 0) { |
| 107 | const panel = createStatePanel(); |
| 108 | document.body.appendChild(panel); |
| 109 | |
| 110 | // For deterministic snapshots: disable animations. |
| 111 | panel.style.setProperty("--stateAnimMs", "0ms"); |
| 112 | |
| 113 | // Ensure panel is expanded so its contents appear in snapshots. |
| 114 | panel.classList.remove("collapsed"); |
| 115 | const edge = panel.querySelector(".state-edge"); |
| 116 | edge?.setAttribute("aria-expanded", "true"); |
| 117 | |
| 118 | const columns = prepareStateVizColumnsFromAmpMap(ampMap, { |
| 119 | maxColumns: maxColumns, |
| 120 | minProbThreshold: minProbThreshold, |
| 121 | }); |
| 122 | updateStatePanelFromColumns(panel, columns, { maxColumns: maxColumns }); |
| 123 | return panel; |
| 124 | } |
| 125 | |
| 126 | test("state viz snapshot - single basis state", async (t) => { |
| 127 | renderStatePanel({ |
| 128 | 0: { re: 1, im: 0 }, |
| 129 | }); |
| 130 | await checkDocumentSnapshot(t, t.name); |
| 131 | }); |
| 132 | |
| 133 | test("state viz snapshot - superposition with phase", async (t) => { |
| 134 | const invSqrt2 = Math.SQRT1_2; |
| 135 | renderStatePanel({ |
| 136 | 0: { re: invSqrt2, im: 0 }, |
| 137 | 1: { re: 0, im: invSqrt2 }, // phase +π/2 |
| 138 | }); |
| 139 | await checkDocumentSnapshot(t, t.name); |
| 140 | }); |
| 141 | |
| 142 | test("state viz snapshot - threshold aggregates to Others", async (t) => { |
| 143 | renderStatePanel( |
| 144 | { |
| 145 | "00": { re: 0.94, im: 0 }, |
| 146 | "01": { re: 0.2, im: 0 }, |
| 147 | 10: { re: 0.1, im: 0 }, |
| 148 | 11: { re: 0.05, im: 0 }, |
| 149 | }, |
| 150 | 8, |
| 151 | 0.05, |
| 152 | ); |
| 153 | await checkDocumentSnapshot(t, t.name); |
| 154 | }); |
| 155 | |
| 156 | // Ensure the cases directory exists when running tests in fresh environments. |
| 157 | if (!fs.existsSync(getCasesDirectory())) { |
| 158 | fs.mkdirSync(getCasesDirectory(), { recursive: true }); |
| 159 | } |
| 160 | |