v-snap 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -2
  3. package/lib/browser/calibration.js +120 -0
  4. package/lib/browser/canvas-calibration.js +553 -0
  5. package/lib/browser/canvas-drawing.d.ts +22 -0
  6. package/lib/browser/canvas-drawing.js +56 -0
  7. package/lib/browser/canvas-text.js +317 -0
  8. package/lib/browser/declaration.js +37 -0
  9. package/lib/browser/fonts/SourceCodeVF-Upright.otf +0 -0
  10. package/lib/browser/fonts/SourceSans3VF-Upright.otf +0 -0
  11. package/lib/browser/fonts/index.js +4 -0
  12. package/lib/browser/fonts/visual-blank.ttf +0 -0
  13. package/lib/browser/html-image.d.ts +14 -0
  14. package/lib/browser/html-image.js +135 -0
  15. package/lib/browser/html-rules.js +149 -0
  16. package/lib/browser/index.d.ts +8 -0
  17. package/lib/browser/index.js +5 -0
  18. package/lib/browser/judge.js +82 -0
  19. package/lib/browser/no-config.d.ts +5 -0
  20. package/lib/browser/no-config.js +5 -0
  21. package/lib/browser/page-setup.js +52 -0
  22. package/lib/browser/page-state.js +22 -0
  23. package/lib/browser/pose.d.ts +8 -0
  24. package/lib/browser/setup.d.ts +1 -0
  25. package/lib/browser/setup.js +9 -0
  26. package/lib/browser/test-options.d.ts +8 -0
  27. package/lib/browser/test-options.js +25 -0
  28. package/lib/browser/view-kinds.d.ts +146 -0
  29. package/lib/browser/view-kinds.js +78 -0
  30. package/lib/browser/visual-test.d.ts +100 -0
  31. package/lib/browser/visual-test.js +323 -0
  32. package/lib/image-name.js +45 -0
  33. package/lib/node/commands.js +264 -0
  34. package/lib/node/compare.js +52 -0
  35. package/lib/node/config.d.ts +69 -0
  36. package/lib/node/config.js +359 -0
  37. package/lib/node/index.d.ts +3 -0
  38. package/lib/node/index.js +3 -0
  39. package/lib/node/install-browser.d.ts +9 -0
  40. package/lib/node/install-browser.js +60 -0
  41. package/lib/node/layout.d.ts +10 -0
  42. package/lib/node/layout.js +108 -0
  43. package/lib/node/paint.js +41 -0
  44. package/lib/node/paths.js +68 -0
  45. package/lib/node/png.js +235 -0
  46. package/lib/node/rebundle.js +92 -0
  47. package/lib/node/references.js +285 -0
  48. package/lib/node/reporter.js +601 -0
  49. package/lib/node/test-files.js +123 -0
  50. package/lib/pixi/index.d.ts +3 -0
  51. package/lib/pixi/index.js +2 -0
  52. package/lib/pixi/pixi-image.d.ts +44 -0
  53. package/lib/pixi/pixi-image.js +125 -0
  54. package/lib/pixi/pixi-views.d.ts +34 -0
  55. package/lib/pixi/pixi-views.js +46 -0
  56. package/lib/protocol.d.ts +52 -0
  57. package/lib/shared/bounded-cache.js +26 -0
  58. package/lib/shared/describe-value.js +10 -0
  59. package/lib/shared/image-budget.d.ts +26 -0
  60. package/lib/shared/image-budget.js +26 -0
  61. package/lib/shared/image-pixels.js +25 -0
  62. package/lib/three/index.d.ts +3 -0
  63. package/lib/three/index.js +2 -0
  64. package/lib/three/three-image.d.ts +46 -0
  65. package/lib/three/three-image.js +143 -0
  66. package/lib/three/three-views.d.ts +27 -0
  67. package/lib/three/three-views.js +37 -0
  68. package/package.json +101 -5
  69. package/src/browser/calibration-images/canvas/canvas-images.png +0 -0
  70. package/src/browser/calibration-images/canvas/canvas-shapes.png +0 -0
  71. package/src/browser/calibration-images/canvas/canvas-text-effects.png +0 -0
  72. package/src/browser/calibration-images/canvas/canvas-text-mono.png +0 -0
  73. package/src/browser/calibration-images/canvas/canvas-text-sans.png +0 -0
  74. package/src/browser/calibration-images/canvas/webgl-aliased.png +0 -0
  75. package/src/browser/calibration-images/canvas/webgl-half-float.png +0 -0
  76. package/src/browser/calibration-images/canvas/webgl-msaa-target.png +0 -0
  77. package/src/browser/calibration-images/canvas/webgl-msaa.png +0 -0
  78. package/src/browser/calibration-images/canvas/webgl-shading.png +0 -0
  79. package/src/browser/calibration-images/canvas/webgl-textures.png +0 -0
  80. package/src/browser/calibration-images/html/blank-text.png +0 -0
  81. package/src/browser/calibration-images/html/boxes.png +0 -0
  82. package/src/browser/calibration-images/html/controls.png +0 -0
  83. package/src/browser/calibration-images/html/filters.png +0 -0
  84. package/src/browser/calibration-images/html/rotated.png +0 -0
  85. package/src/browser/fonts/LICENSE-source-code-pro.md +93 -0
  86. package/src/browser/fonts/LICENSE-source-sans.md +93 -0
  87. package/visual-environment.json +7 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Troy Gerwien
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,228 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="docs/images/logo.svg" width="110" height="92" alt="">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ <h1 align="center">vsnap</h1>
6
+
7
+ <p align="center">
8
+ <strong>Visual tests as fast and as dependable as unit tests.</strong><br>
9
+ For views drawn with Pixi, three.js, any canvas, or HTML.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <img src="https://img.shields.io/badge/Vitest-5-6E9F18?logo=vitest&logoColor=white" alt="Vitest 5">
14
+ <img src="https://img.shields.io/badge/Playwright-1.63-2EAD33" alt="Playwright 1.63">
15
+ <img src="https://img.shields.io/badge/Node-22.17%2B-5FA04E?logo=nodedotjs&logoColor=white" alt="Node 22.17 or later">
16
+ <img src="https://img.shields.io/badge/types-TypeScript-3178C6?logo=typescript&logoColor=white" alt="TypeScript types">
17
+ <img src="https://img.shields.io/badge/licence-MIT-7c5cff" alt="MIT licence">
18
+ </p>
19
+
20
+ `vsnap` makes visual testing as convenient as unit testing. A visual test
21
+ builds a view in a known state, renders it to an image, and compares that
22
+ image with a reference image, which is an accepted image committed beside
23
+ the test file. It is a kind of snapshot test, also known as a visual
24
+ regression test. vsnap runs on Vitest's browser mode, so visual tests are
25
+ Vitest tests. It is not published yet.
26
+
27
+ ## Why vsnap
28
+
29
+ - **Fast.** A canvas image takes about 4 ms to draw and check, 17 times
30
+ less than a screenshot test in the same browser. 1000 images run in under
31
+ 8 seconds, start-up included.
32
+ - **The same images on every machine.** Windows, Linux and macOS, on x64
33
+ and arm64 processors, draw the same images. A reference made on one
34
+ passes on the others.
35
+ - **No Docker, no setup.** `npm ci` installs everything but the browser,
36
+ which the first run downloads.
37
+ - **Tests read like unit tests.** One line declares one image, and a new
38
+ test needs no other change. There is no test page to build.
39
+ - **Pixi, three.js, any canvas, and HTML.** vsnap draws Pixi and three.js
40
+ views itself, and support for another renderer is a small object. HTML
41
+ views are tested too.
42
+ - **Vitest's tools work.** Watch mode reruns a test when its view changes.
43
+ The Vitest UI shows a failed image beside its reference, and the VS Code
44
+ extension lists and runs the tests.
45
+
46
+ ## How Fast?
47
+
48
+ The same 1000 images of Pixi views, checked with vsnap and with Vitest's
49
+ own `toMatchScreenshot`, in the same browser:
50
+
51
+ | | 1000 images, start-up included | One image, at the median |
52
+ | --- | ---: | ---: |
53
+ | **vsnap**, in one page | **7.8 s** | **4.0 ms** |
54
+ | `toMatchScreenshot`, in one page | 89.9 s (11.5x) | 67 ms (17x) |
55
+ | `toMatchScreenshot`, 13 pages at once | 18.0 s (2.3x) | 131 ms (33x) |
56
+
57
+ Like for like, in one browser page each, vsnap runs the whole suite 11.5
58
+ times as fast, and each image 17 times as fast. By default, Vitest opens a
59
+ page for each processor core but one, and runs that many test files at
60
+ once. On the 14-core laptop measured, that is 13 pages, which share the
61
+ processor, so each screenshot takes longer. Even then, vsnap in one page
62
+ is 2.3 times as fast. A machine with fewer cores, such as a CI runner,
63
+ runs fewer pages at once, so there the gap should be wider.
64
+
65
+ [The benchmark](docs/how-it-works.md#the-benchmark) says how it was
66
+ measured, and how to run it.
67
+
68
+ ## A Taste
69
+
70
+ A test file sits in a `visual-tests` folder beside its view. Each test
71
+ gives a name, and a pose: a function that returns the view in the state
72
+ that the test describes.
73
+
74
+ ```tsx
75
+ // src/hud/visual-tests/hud-view.test.tsx
76
+ import { canvasTest } from 'vsnap';
77
+ import { HudView } from '../hud-view';
78
+
79
+ canvasTest('at the start', () => HudView({ score: () => 0 }));
80
+ canvasTest('a high score', () => HudView({ score: () => 98_765 }));
81
+ ```
82
+
83
+ The first run writes each image as its test's reference, beside the test
84
+ file, as `hud-view.at-the-start.png` and `hud-view.a-high-score.png`. Look
85
+ at them, and commit them. From then on, each run compares the images with
86
+ them.
87
+
88
+ ## When an Image Changes
89
+
90
+ A test whose image changed fails, and vsnap writes its actual image and a
91
+ diff. The diff shows the reference dimmed, and the pixels that changed in
92
+ red. Here, one more light is on:
93
+
94
+ | The reference | The actual image | The diff |
95
+ | :---: | :---: | :---: |
96
+ | <img src="docs/images/example-reference.png" width="250" alt="A panel, with three of its lights on at the left, and two at the right"> | <img src="docs/images/example-actual.png" width="250" alt="The same panel, with a fourth light on in the middle"> | <img src="docs/images/example-diff.png" width="250" alt="The panel dimmed, with the new light in red"> |
97
+
98
+ The failure message says where the image changed, and by how much:
99
+
100
+ ```
101
+ Image 'a-panel' differs from its reference in 214 pixels, by up to 194 of 255,
102
+ in the 16x16 pixels at (116, 108).
103
+ ```
104
+
105
+ It goes on to name the three files, and ends with the command that
106
+ accepts the change: `vitest run <file> -u`. The Vitest UI shows the same
107
+ three images, with a slider between the reference and the actual image.
108
+
109
+ ## Quick Start
110
+
111
+ 1. Install vsnap beside Vitest. npm also installs Playwright and Vitest's
112
+ Playwright provider, which vsnap needs.
113
+
114
+ ```sh
115
+ npm install --save-dev vsnap vitest
116
+ ```
117
+
118
+ 2. List vsnap's projects in `vitest.config.ts`, after a project for the
119
+ unit tests, which leaves out the visual tests.
120
+
121
+ ```ts
122
+ import { configDefaults, defineConfig } from 'vitest/config';
123
+ import { vsnap } from 'vsnap/vitest';
124
+
125
+ export default defineConfig({
126
+ test: {
127
+ projects: [
128
+ {
129
+ extends: true,
130
+ test: {
131
+ name: 'unit',
132
+ exclude: [...configDefaults.exclude, '**/visual-tests/**'],
133
+ },
134
+ },
135
+ ...vsnap(),
136
+ ],
137
+ },
138
+ });
139
+ ```
140
+
141
+ 3. Register the kinds of view that the tests pose in `vsnap.config.ts`,
142
+ beside it, and add the file to `tsconfig.json`'s `include`.
143
+
144
+ ```ts
145
+ import { defineVsnapConfig } from 'vsnap';
146
+ import { pixiViews } from 'vsnap/pixi';
147
+
148
+ const config = defineVsnapConfig({ views: [pixiViews()], defaultKind: 'pixi' });
149
+
150
+ // Gives canvasTest's types the kinds of view above, and the default kind.
151
+ declare module 'vsnap' {
152
+ interface Register {
153
+ config: typeof config;
154
+ }
155
+ }
156
+
157
+ export default config;
158
+ ```
159
+
160
+ 4. Add `.vitest/` to `.gitignore`. vsnap writes a failed test's images
161
+ there.
162
+
163
+ 5. Write a test, as above, and run `npx vitest`.
164
+
165
+ [Getting Started](docs/getting-started.md) covers a project whose config
166
+ already has settings, and what vsnap needs.
167
+
168
+ ## Documentation
169
+
170
+ - **[Getting Started](docs/getting-started.md)**: the requirements, adding
171
+ vsnap to a project, and the first test.
172
+ - **[Writing Tests](docs/writing-tests.md)**: declaring tests, views that
173
+ change over time, options, tables of cases, image names, and how HTML
174
+ views are captured.
175
+ - **[Kinds of View](docs/kinds-of-view.md)**: Pixi, three.js, canvases and
176
+ HTML, the options of each, preparing and disposing of views, configs in
177
+ a monorepo, and support for another renderer.
178
+ - **[Configuration](docs/configuration.md)**: `vsnap()`'s options, what
179
+ its projects inherit, and where test files and references go.
180
+ - **[Running the Tests](docs/running-tests.md)**: the commands, the checks
181
+ of the references, the summary, reviewing a failed image, CI and watch
182
+ mode.
183
+ - **[How It Works](docs/how-it-works.md)**: what makes vsnap fast, the
184
+ benchmark, and how it gives the same images on every machine.
185
+ - **[Limits and Rules](docs/limits-and-rules.md)**: what the images leave
186
+ out, and the rules that tests follow.
187
+ - **[Troubleshooting](docs/troubleshooting.md)**: vsnap's messages, and
188
+ what they usually mean.
189
+
190
+ ## API
191
+
192
+ | Import | Export | What it is |
193
+ | --- | --- | --- |
194
+ | `vsnap` | `canvasTest(name, options?, pose)` | Declares a test of a view drawn on a canvas. It has Vitest's modifiers, such as `.skip`, `.only` and `.each` |
195
+ | | `htmlTest(name, options?, pose)` | Declares a test of an HTML view, with the same modifiers |
196
+ | | `defineVsnapConfig(config)` | Returns the config that `vsnap.config.ts` exports, with its kinds of view and its default kind |
197
+ | | `bindCanvasTest(config)` | Returns a `canvasTest` whose types know one config's kinds of view, for a monorepo's packages |
198
+ | | `htmlViews(options)` | Returns the kind for HTML views, with steps to prepare and dispose of them, and a background for every test |
199
+ | | `fitImage`, `readCanvas` | Helpers for support for another renderer: how an image fits the size budget, and a canvas's pixels |
200
+ | `vsnap/vitest` | `vsnap(options?)` | Returns the Vitest projects that run the visual tests, to list in `test.projects` |
201
+ | `vsnap/pixi` | `pixiViews(options?)` | Returns the kind for Pixi views |
202
+ | `vsnap/three` | `threeViews(options?)` | Returns the kind for three.js views |
203
+
204
+ ## Requirements
205
+
206
+ | Needs | Version |
207
+ | --- | --- |
208
+ | Vitest, with `@vitest/browser-playwright` | 5 |
209
+ | Playwright, installed with vsnap | 1.63, any patch release |
210
+ | Node | 22.17 or later |
211
+ | Pixi, for Pixi views | 8.16 or later |
212
+ | three.js, for three.js views | 0.160 or later |
213
+
214
+ A canvas test can draw with the browser's own canvas, and needs no
215
+ renderer library. [Getting Started](docs/getting-started.md#requirements)
216
+ says why each is needed.
217
+
218
+ ## Contributing
219
+
220
+ To work on vsnap itself, see [CONTRIBUTING.md](./CONTRIBUTING.md). It says
221
+ how vsnap's own tests run, how the package is laid out, how the test
222
+ environment is checked and upgraded, and how the package is built and
223
+ published.
224
+
225
+ ## Licence
226
+
227
+ MIT. The fonts in `src/browser/fonts/` are under the SIL Open Font
228
+ License, whose text is beside them.
@@ -0,0 +1,120 @@
1
+ import { isAllOne } from "../shared/image-pixels.js";
2
+ import { describeDifference, hashPixels, isPass, openSession, toBase64, visualCommands } from "./judge.js";
3
+ import { captureHtmlImage } from "./html-image.js";
4
+ import { CANVAS_CALIBRATION, SAMPLE } from "./canvas-calibration.js";
5
+ import { inject } from "vitest";
6
+ //#region src/browser/calibration.ts
7
+ /**
8
+ * Checks that this machine is the reference environment, before any test
9
+ * runs. First it checks the browser's facts. Then it draws a calibration set
10
+ * of small images. Each one exercises one way that an image can differ
11
+ * between machines. If anything differs beyond the tolerance, the run stops
12
+ * with one error that says what differs. That is clearer than failing every
13
+ * test for one reason that nobody can see. In `environment` mode, it writes
14
+ * the facts and the calibration set's references instead. It runs once per
15
+ * run for each kind of page.
16
+ */
17
+ async function checkCalibration(pageKind) {
18
+ const session = await openSession({ calibration: pageKind });
19
+ if (session.isCalibrated) return;
20
+ if (!navigator.userAgent.includes("HeadlessChrome")) {
21
+ const message = `The visual tests (${pageKind}) run in Playwright's headless Chromium, which vsnap sets up, but this page runs in another browser (${navigator.userAgent}), such as a headed one, with --browser.headless=false, or one that the config names. It would draw the images differently, so no actual image of a test was compared. Run the visual tests headless. To look at a view, open its actual image in .vitest/vsnap/, or the failed test in the Vitest UI.`;
22
+ await visualCommands.abortVisualRun(message);
23
+ throw new Error(message);
24
+ }
25
+ const problems = [...await visualCommands.checkVisualEnvironment(readEnvironment())];
26
+ const calibrations = pageKind === "canvas" ? CANVAS_JUDGED : HTML_CALIBRATION;
27
+ if (problems.length === 0) for (const [name, draw] of Object.entries(calibrations)) {
28
+ const verdict = await draw(name, session.hashes[name]);
29
+ if (!isPass(verdict)) {
30
+ const detail = verdict.outcome === "differs" ? describeDifference(verdict) : verdict.outcome;
31
+ problems.push(`the calibration image '${name}' differs (${detail}), as ${verdict.diffFile ?? verdict.actualFile} shows`);
32
+ }
33
+ }
34
+ if (problems.length > 0) {
35
+ const message = `This machine is not the visual tests' reference environment (${pageKind}). These checks failed: ${problems.join("; ")}. No test's actual image was compared. vsnap draws with the browser of one Playwright version, so check that the project installs the Playwright version that vsnap's package.json names, and only one copy of it.`;
36
+ await visualCommands.abortVisualRun(message);
37
+ throw new Error(message);
38
+ }
39
+ await visualCommands.recordVisualCalibration({
40
+ pageKind,
41
+ names: Object.keys(calibrations)
42
+ });
43
+ }
44
+ /**
45
+ * Returns a canvas calibration image that judges itself against its
46
+ * expected hash. Only an image whose hash differs goes to Node.
47
+ */
48
+ function judgeCanvasCalibration(draw) {
49
+ return async (name, expectedHash) => {
50
+ const image = draw();
51
+ if (isAllOne(image.pixels)) throw new Error(`The calibration image '${name}' is blank. Something it draws with failed on this machine.`);
52
+ const hash = await hashPixels(image.width, image.height, image.pixels);
53
+ if (hash === expectedHash) return {
54
+ outcome: "same",
55
+ referenceFile: name
56
+ };
57
+ return visualCommands.judgeVisualMismatch({
58
+ calibration: "canvas",
59
+ name,
60
+ test: `calibration ${name}`,
61
+ width: image.width,
62
+ height: image.height,
63
+ hash,
64
+ pixels: toBase64(image.pixels)
65
+ });
66
+ };
67
+ }
68
+ const CANVAS_JUDGED = Object.fromEntries(Object.entries(CANVAS_CALIBRATION).map(([name, draw]) => [name, judgeCanvasCalibration(draw)]));
69
+ function readEnvironment() {
70
+ const gl = document.createElement("canvas").getContext("webgl2");
71
+ const info = gl?.getExtension("WEBGL_debug_renderer_info");
72
+ return {
73
+ browser: /HeadlessChrome\/[\d.]+/.exec(navigator.userAgent)?.[0] ?? navigator.userAgent,
74
+ webglRenderer: info !== void 0 && info !== null && gl !== null ? String(gl.getParameter(info.UNMASKED_RENDERER_WEBGL)) : "none",
75
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
76
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
77
+ devicePixelRatio
78
+ };
79
+ }
80
+ /** Creates a calibration image made of HTML markup. */
81
+ function createHtmlCalibration(markup) {
82
+ return async (name) => {
83
+ const root = document.createElement("div");
84
+ root.style.cssText = "padding:8px;color:#e6edf3;font-size:14px;";
85
+ root.innerHTML = markup;
86
+ return captureHtmlImage(root, { maxPixels: inject("visualMaxPixels") }, {
87
+ calibration: "html",
88
+ name,
89
+ test: `calibration ${name}`
90
+ });
91
+ };
92
+ }
93
+ const HTML_CALIBRATION = {
94
+ "blank-text": createHtmlCalibration([
95
+ 10,
96
+ 13,
97
+ 13.5,
98
+ 16,
99
+ 17.3,
100
+ 24
101
+ ].map((size) => `<div style="font-size:${size}px;width:max-content;border:1px solid #58a6ff;margin:2px 0">${SAMPLE}</div>`).join("") + "<p style=\"width:200px;border:1px solid #ffe45c;line-height:1.4\">Wrapping text, with emoji 😀 and 漢字, in a narrow box.</p>"),
102
+ "boxes": createHtmlCalibration(`<div style="display:flex;gap:16px">
103
+ <div style="width:90px;height:60px;border-radius:14px 4px 22px 8px;background:linear-gradient(135deg,#ff4f8b,#ffe45c)"></div>
104
+ <div style="width:60px;height:60px;border-radius:50%;background:radial-gradient(circle,#fff,#2a1766);box-shadow:0 6px 14px rgba(0,0,0,.6)"></div>
105
+ <div style="width:60px;height:40px;border:3px dashed #5bd1ff;outline:2px solid #58a6ff;outline-offset:3px"></div></div>`),
106
+ "rotated": createHtmlCalibration(`<div style="display:flex;gap:24px;padding:12px">
107
+ <div style="width:60px;height:40px;background:#ffe45c;transform:rotate(7deg)"></div>
108
+ <div style="perspective:200px"><div style="width:60px;height:40px;background:#ff4f8b;transform:rotateY(35deg)"></div></div>
109
+ <img style="width:80px;height:60px;transform:rotate(-3deg)" src="data:image/svg+xml,${encodeURIComponent("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"30\"><rect width=\"40\" height=\"30\" fill=\"#2a1766\"/><circle cx=\"20\" cy=\"15\" r=\"10\" fill=\"#5bd1ff\"/></svg>")}"></div>`),
110
+ "filters": createHtmlCalibration(`<div style="display:flex;gap:18px;padding:10px;background:linear-gradient(90deg,#123,#456)">
111
+ <div style="width:50px;height:50px;background:#ffe45c;filter:blur(3px)"></div>
112
+ <div style="width:50px;height:50px;background:#5bd1ff;filter:drop-shadow(4px 4px 3px #000)"></div>
113
+ <div style="width:50px;height:50px;backdrop-filter:blur(4px);border:1px solid #fff"></div></div>`),
114
+ "controls": createHtmlCalibration(`<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;width:520px">
115
+ <input type="checkbox" checked><input type="radio" checked><input type="range" value="30">
116
+ <progress value="0.4"></progress><button>Button</button><select><option>Option</option></select>
117
+ <input type="text" value="Text field"><textarea rows="2" cols="12">Two lines</textarea></div>`)
118
+ };
119
+ //#endregion
120
+ export { checkCalibration };