@uiverify/vitest 1.0.2 → 1.2.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.
@@ -1,11 +1,34 @@
1
1
  import {
2
2
  autoSnapshot,
3
- beginTest
4
- } from "./chunk-W4AFNW3N.js";
3
+ beginTest,
4
+ preloadFonts
5
+ } from "./chunk-ZJM4XSRA.js";
5
6
 
6
7
  // src/browser-setup.ts
7
8
  import { afterEach, beforeEach, inject } from "vitest";
9
+
10
+ // src/seed.ts
11
+ var INITIAL_SEED = 2685821657736338400;
12
+ var seed = INITIAL_SEED;
13
+ var installed = false;
14
+ function seededRandom() {
15
+ seed ^= seed << 13;
16
+ seed ^= seed >> 7;
17
+ seed ^= seed << 17;
18
+ return (seed >>> 0) % 1e9 / 1e9;
19
+ }
20
+ function installSeededRandom() {
21
+ if (installed) return;
22
+ installed = true;
23
+ Math.random = seededRandom;
24
+ }
25
+ function resetSeed() {
26
+ if (installed) seed = INITIAL_SEED;
27
+ }
28
+
29
+ // src/browser-setup.ts
8
30
  Reflect.set(globalThis, "__UI_VERIFY__", true);
31
+ installSeededRandom();
9
32
  function globalAutoDisabled() {
10
33
  try {
11
34
  return Boolean(inject("__uiverify")?.disableAutoSnapshot);
@@ -13,7 +36,9 @@ function globalAutoDisabled() {
13
36
  return false;
14
37
  }
15
38
  }
16
- beforeEach((ctx) => {
39
+ beforeEach(async (ctx) => {
40
+ resetSeed();
41
+ await preloadFonts();
17
42
  beginTest(ctx.task);
18
43
  });
19
44
  afterEach(async (ctx) => {
@@ -1,3 +1,34 @@
1
+ // src/settle.ts
2
+ var SETTLE_TIMEOUT_MS = 3e3;
3
+ var realSetTimeout = typeof setTimeout === "function" ? setTimeout.bind(globalThis) : () => 0;
4
+ function timeout(ms) {
5
+ return new Promise((resolve) => realSetTimeout(() => resolve(), ms));
6
+ }
7
+ function cap(work) {
8
+ return Promise.race([work, timeout(SETTLE_TIMEOUT_MS)]);
9
+ }
10
+ async function preloadFonts() {
11
+ if (typeof document === "undefined" || !document.fonts) return;
12
+ const loadAll = Promise.all(Array.from(document.fonts, (f) => f.load().catch(() => void 0)));
13
+ await cap(loadAll.then(() => document.fonts.ready));
14
+ }
15
+ async function settle() {
16
+ if (typeof document === "undefined") return;
17
+ await preloadFonts();
18
+ const pending = Array.from(document.images).filter((img) => !img.complete);
19
+ if (!pending.length) return;
20
+ await cap(
21
+ Promise.all(
22
+ pending.map(
23
+ (img) => new Promise((resolve) => {
24
+ img.addEventListener("load", () => resolve(), { once: true });
25
+ img.addEventListener("error", () => resolve(), { once: true });
26
+ })
27
+ )
28
+ )
29
+ );
30
+ }
31
+
1
32
  // src/capture.ts
2
33
  import { commands } from "@vitest/browser/context";
3
34
  import { snapshot } from "rrweb-snapshot";
@@ -26,7 +57,7 @@ function snapshotIds(task, name) {
26
57
  // package.json
27
58
  var package_default = {
28
59
  name: "@uiverify/vitest",
29
- version: "1.0.2",
60
+ version: "1.2.0",
30
61
  description: "Vitest browser-mode capture SDK for UI Verify (uiverify.ai): add uiverifyPlugin() to your Vitest config and each browser-mode test archives its final DOM (serialized DOM + resource bytes) to be replayed + visually diffed by UI Verify. Produces an archive the uiverify CLI uploads; talks to nothing at runtime.",
31
62
  keywords: [
32
63
  "visual-testing",
@@ -144,6 +175,7 @@ async function collectResources() {
144
175
  }
145
176
  async function capture(task, name) {
146
177
  const { id, title } = snapshotIds(task, name);
178
+ await settle();
147
179
  const serialize = (recordCanvas) => snapshot(document, { inlineStylesheet: true, recordCanvas, slimDOM: { script: true } });
148
180
  let dom;
149
181
  try {
@@ -164,7 +196,11 @@ async function capture(task, name) {
164
196
  colorScheme,
165
197
  dom,
166
198
  resources,
167
- producer: PRODUCER
199
+ producer: PRODUCER,
200
+ // The test file (project-root-relative), so UI Verify can locate this snapshot in the module graph
201
+ // for skip-unchanged. `task.file.name` is already root-relative — the same value snapshot ids derive
202
+ // their first segment from — so it lines up with the graph names the reporter emits.
203
+ ...task.file?.name ? { sourcePath: task.file.name } : {}
168
204
  };
169
205
  await commands.__uiverifyWriteSnapshot(archived);
170
206
  return id;
@@ -197,6 +233,7 @@ async function autoSnapshot(passed) {
197
233
  }
198
234
 
199
235
  export {
236
+ preloadFonts,
200
237
  beginTest,
201
238
  takeSnapshot,
202
239
  disableAutoSnapshot,
package/dist/index.d.ts CHANGED
@@ -6,4 +6,9 @@ declare function takeSnapshot(name?: string): Promise<string>;
6
6
  /** Turn off the automatic end-of-test snapshot for the current test only. */
7
7
  declare function disableAutoSnapshot(): void;
8
8
 
9
- export { disableAutoSnapshot, takeSnapshot };
9
+ /** Load every registered `@font-face` and wait for the font set to be ready. Exported so a test can call
10
+ * it BEFORE `render()` for a component that measures text width on mount - a mid-load font bakes a wrong
11
+ * metric (a 1px-shifted layout) that settling AFTER render can no longer undo. */
12
+ declare function preloadFonts(): Promise<void>;
13
+
14
+ export { disableAutoSnapshot, preloadFonts, takeSnapshot };
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  disableAutoSnapshot,
3
+ preloadFonts,
3
4
  takeSnapshot
4
- } from "./chunk-W4AFNW3N.js";
5
+ } from "./chunk-ZJM4XSRA.js";
5
6
  export {
6
7
  disableAutoSnapshot,
8
+ preloadFonts,
7
9
  takeSnapshot
8
10
  };
package/dist/plugin.js CHANGED
@@ -31,6 +31,115 @@ function writeSnapshot(outDir, snapshot) {
31
31
  import fs2 from "fs";
32
32
  import path3 from "path";
33
33
 
34
+ // src/graph-reporter.ts
35
+ import fs3 from "fs";
36
+ import path5 from "path";
37
+
38
+ // src/preview-stats.ts
39
+ import path4 from "path";
40
+ function toRepoRel(viteRoot, raw) {
41
+ if (!raw) return null;
42
+ const p = raw.replace(/\?.*$/, "");
43
+ if (p.includes("\0") || p.startsWith("virtual:")) return null;
44
+ if (!path4.isAbsolute(p)) return null;
45
+ if (/(^|[/\\])node_modules[/\\]/.test(p)) return null;
46
+ return path4.relative(viteRoot, p).split(path4.sep).join("/");
47
+ }
48
+ function buildPreviewStats(input) {
49
+ const reasonsByName = /* @__PURE__ */ new Map();
50
+ const ensure = (name) => {
51
+ let set = reasonsByName.get(name);
52
+ if (!set) reasonsByName.set(name, set = /* @__PURE__ */ new Set());
53
+ return set;
54
+ };
55
+ for (const mod of input.modules) {
56
+ const name = toRepoRel(input.viteRoot, mod.file ?? mod.id);
57
+ if (name === null) continue;
58
+ const reasons = ensure(name);
59
+ for (const imp of mod.importers ?? []) {
60
+ const r = toRepoRel(input.viteRoot, imp.file ?? imp.id);
61
+ if (r !== null && r !== name) reasons.add(r);
62
+ }
63
+ }
64
+ const testNames = input.testFiles.map((f) => toRepoRel(input.viteRoot, f)).filter((n) => n !== null);
65
+ for (const sf of input.setupFiles) {
66
+ const name = toRepoRel(input.viteRoot, sf);
67
+ if (name === null) continue;
68
+ const reasons = ensure(name);
69
+ for (const t of testNames) if (t !== name) reasons.add(t);
70
+ }
71
+ return {
72
+ modules: [...reasonsByName].map(([name, reasons]) => ({
73
+ name,
74
+ reasons: [...reasons].map((moduleName) => ({ moduleName }))
75
+ }))
76
+ };
77
+ }
78
+
79
+ // src/graph-reporter.ts
80
+ function collectModules(map, out) {
81
+ if (typeof map?.forEach !== "function") return;
82
+ map.forEach((mod) => out.push(mod));
83
+ }
84
+ function buildStatsFromRun(vitest, testModules) {
85
+ const projects = vitest?.projects ?? [];
86
+ if (projects.length === 0) return null;
87
+ const testFilesByProject = /* @__PURE__ */ new Map();
88
+ for (const tm of testModules ?? []) {
89
+ if (!tm.project || !tm.moduleId) continue;
90
+ const arr = testFilesByProject.get(tm.project) ?? [];
91
+ arr.push(tm.moduleId);
92
+ testFilesByProject.set(tm.project, arr);
93
+ }
94
+ const reasonsByName = /* @__PURE__ */ new Map();
95
+ let any = false;
96
+ for (const project of projects) {
97
+ const vite = project.browser?.vite;
98
+ const viteRoot = vite?.config?.root;
99
+ if (!vite || !viteRoot) continue;
100
+ const modules = [];
101
+ collectModules((vite.environments?.client?.moduleGraph ?? vite.moduleGraph)?.idToModuleMap, modules);
102
+ if (modules.length === 0) continue;
103
+ const testFiles = [.../* @__PURE__ */ new Set([...testFilesByProject.get(project) ?? [], ...project.testFilesList ?? []])];
104
+ const stats = buildPreviewStats({ viteRoot, modules, setupFiles: project.config?.setupFiles ?? [], testFiles });
105
+ for (const m of stats.modules) {
106
+ let set = reasonsByName.get(m.name);
107
+ if (!set) reasonsByName.set(m.name, set = /* @__PURE__ */ new Set());
108
+ for (const r of m.reasons) set.add(r.moduleName);
109
+ }
110
+ any = true;
111
+ }
112
+ if (!any) return null;
113
+ return {
114
+ modules: [...reasonsByName].map(([name, reasons]) => ({
115
+ name,
116
+ reasons: [...reasons].map((moduleName) => ({ moduleName }))
117
+ }))
118
+ };
119
+ }
120
+ function emitModuleGraph(outDir, vitest, testModules) {
121
+ const stats = buildStatsFromRun(vitest, testModules);
122
+ if (!stats) return false;
123
+ fs3.mkdirSync(outDir, { recursive: true });
124
+ fs3.writeFileSync(path5.join(outDir, "preview-stats.json"), JSON.stringify(stats));
125
+ return true;
126
+ }
127
+ function graphReporter(outDir) {
128
+ let vitest;
129
+ return {
130
+ onInit(ctx) {
131
+ vitest = ctx;
132
+ },
133
+ onTestRunEnd(testModules) {
134
+ try {
135
+ emitModuleGraph(outDir, vitest, testModules);
136
+ } catch (err) {
137
+ console.warn("[uiverify] could not emit the module graph for --only-changed:", err);
138
+ }
139
+ }
140
+ };
141
+ }
142
+
34
143
  // src/plugin.ts
35
144
  var SETUP_MODULE = "@uiverify/vitest/browser-setup";
36
145
  function uiverifyPlugin(options = {}) {
@@ -44,6 +153,11 @@ function uiverifyPlugin(options = {}) {
44
153
  optimizeDeps: { exclude: ["@uiverify/vitest"] },
45
154
  test: {
46
155
  setupFiles: [SETUP_MODULE],
156
+ // Emit Vite's module graph as `preview-stats.json` so `uiverify upload --only-changed` can skip
157
+ // unchanged snapshots. `"default"` is kept so the plugin doesn't silence Vitest's own output. If a
158
+ // user's own `reporters` config ends up displacing this, the only effect is that no graph ships and
159
+ // the server safely renders everything — add `graphReporter()` back to keep the skip savings.
160
+ reporters: ["default", graphReporter(outDir)],
47
161
  provide: { __uiverify: { disableAutoSnapshot: options.disableAutoSnapshot ?? false } },
48
162
  browser: {
49
163
  commands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uiverify/vitest",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Vitest browser-mode capture SDK for UI Verify (uiverify.ai): add uiverifyPlugin() to your Vitest config and each browser-mode test archives its final DOM (serialized DOM + resource bytes) to be replayed + visually diffed by UI Verify. Produces an archive the uiverify CLI uploads; talks to nothing at runtime.",
5
5
  "keywords": [
6
6
  "visual-testing",