vitest-plugin-vis 5.0.0 → 5.1.1

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 (109) hide show
  1. package/dist/auto_snapshots/_extract_auto_snapshot_options.mjs +1 -1
  2. package/dist/auto_snapshots/_extract_auto_snapshot_options.mjs.map +1 -1
  3. package/dist/client/external/vitest/vitest_suite_proxy.d.mts +0 -1
  4. package/dist/client/snapshot/compare_image_snapshot.logic.mjs +2 -1
  5. package/dist/client/snapshot/compare_image_snapshot.logic.mjs.map +1 -1
  6. package/dist/client/snapshot/compare_image_snapshot.mjs +1 -1
  7. package/dist/client/snapshot/compare_image_snapshot.mjs.map +1 -1
  8. package/dist/client/suite/auto_snapshot_matcher.d.mts +2 -2
  9. package/dist/client/suite/auto_snapshot_matcher.d.mts.map +1 -1
  10. package/dist/client/suite/auto_snapshot_matcher.mjs +5 -3
  11. package/dist/client/suite/auto_snapshot_matcher.mjs.map +1 -1
  12. package/dist/config/types.d.mts +14 -0
  13. package/dist/{client-api.d.mts → exports/client-api.d.mts} +12 -12
  14. package/dist/exports/client-api.mjs +11 -0
  15. package/dist/exports/config.d.mts +5 -0
  16. package/dist/exports/config.mjs +4 -0
  17. package/dist/exports/presets/auto.mjs +9 -0
  18. package/dist/exports/presets/auto.mjs.map +1 -0
  19. package/dist/exports/presets/manual.mjs +9 -0
  20. package/dist/exports/presets/manual.mjs.map +1 -0
  21. package/dist/exports/server-utils.d.mts +5 -0
  22. package/dist/exports/server-utils.mjs +6 -0
  23. package/dist/exports/setup-api.d.mts +4 -0
  24. package/dist/exports/setup-api.mjs +5 -0
  25. package/dist/exports/setup.d.mts +3 -0
  26. package/dist/exports/setup.mjs +4 -0
  27. package/dist/exports/testing/node.d.mts +4 -0
  28. package/dist/exports/testing/node.mjs +5 -0
  29. package/dist/exports/testing.d.mts +2 -0
  30. package/dist/exports/testing.mjs +3 -0
  31. package/dist/server/commands/prepare_image_snapshot_comparison.mjs.map +1 -1
  32. package/dist/server/commands/prepare_page_image_snapshot_comparison.mjs.map +1 -1
  33. package/dist/server/snapshot_path.d.mts +0 -1
  34. package/dist/server/snapshot_path.d.mts.map +1 -1
  35. package/dist/server/snapshot_path_limits.d.mts +28 -0
  36. package/dist/server/snapshot_path_limits.d.mts.map +1 -0
  37. package/dist/server/snapshot_path_limits.mjs +92 -0
  38. package/dist/server/snapshot_path_limits.mjs.map +1 -0
  39. package/dist/server/suite.mjs +8 -2
  40. package/dist/server/suite.mjs.map +1 -1
  41. package/dist/server/testing/stubBrowserCommandContext.mjs +9 -0
  42. package/dist/server/testing/stubBrowserCommandContext.mjs.map +1 -0
  43. package/dist/server/testing/stubSuite.d.mts +17 -0
  44. package/dist/server/testing/stubSuite.d.mts.map +1 -0
  45. package/dist/server/testing/stubSuite.mjs +23 -0
  46. package/dist/server/testing/stubSuite.mjs.map +1 -0
  47. package/dist/server/testing/stubUserConfig.mjs +8 -0
  48. package/dist/server/testing/stubUserConfig.mjs.map +1 -0
  49. package/dist/server/vis_options.d.mts +120 -0
  50. package/dist/server/vis_options.d.mts.map +1 -0
  51. package/dist/server/vis_options.mjs +7 -1
  52. package/dist/server/vis_options.mjs.map +1 -1
  53. package/dist/server/vis_server_context.logic.mjs +37 -10
  54. package/dist/server/vis_server_context.logic.mjs.map +1 -1
  55. package/dist/server/vis_server_context.types.d.mts +32 -1
  56. package/dist/server/vis_server_context.types.d.mts.map +1 -0
  57. package/dist/setup/create_vis.mjs +6 -6
  58. package/dist/setup/create_vis.mjs.map +1 -1
  59. package/dist/shared/types.d.mts +21 -1
  60. package/dist/shared/types.d.mts.map +1 -1
  61. package/package.json +13 -36
  62. package/readme.md +6 -0
  63. package/src/auto_snapshots/_extract_auto_snapshot_options.ts +1 -1
  64. package/src/client/expect/to_match_image_snapshot.mdx +24 -1
  65. package/src/client/page/to_match_image_snapshot.mdx +23 -1
  66. package/src/client/snapshot/compare_image_snapshot.logic.ts +1 -0
  67. package/src/client/snapshot/compare_image_snapshot.ts +2 -2
  68. package/src/client/suite/auto_snapshot_matcher.ts +10 -2
  69. package/src/config/types.ts +14 -0
  70. package/src/exports/client-api.ts +13 -0
  71. package/src/exports/config.ts +3 -0
  72. package/src/exports/server-utils.ts +7 -0
  73. package/src/exports/setup-api.ts +5 -0
  74. package/src/exports/setup.ts +3 -0
  75. package/src/exports/testing/node.ts +4 -0
  76. package/src/exports/testing.ts +1 -0
  77. package/src/server/commands/prepare_image_snapshot_comparison.ts +2 -2
  78. package/src/server/commands/prepare_page_image_snapshot_comparison.ts +2 -1
  79. package/src/server/snapshot_path_limits.ts +135 -0
  80. package/src/server/suite.ts +19 -4
  81. package/src/server/vis_options.ts +10 -1
  82. package/src/server/vis_server_context.logic.ts +47 -17
  83. package/src/setup/create_vis.ts +12 -11
  84. package/src/shared/types.ts +21 -1
  85. package/dist/client-api.mjs +0 -11
  86. package/dist/config.d.mts +0 -5
  87. package/dist/config.mjs +0 -4
  88. package/dist/presets/auto.mjs +0 -9
  89. package/dist/presets/auto.mjs.map +0 -1
  90. package/dist/presets/manual.mjs +0 -9
  91. package/dist/presets/manual.mjs.map +0 -1
  92. package/dist/server-utils.d.mts +0 -4
  93. package/dist/server-utils.mjs +0 -5
  94. package/dist/setup-api.d.mts +0 -4
  95. package/dist/setup-api.mjs +0 -5
  96. package/dist/setup.d.mts +0 -3
  97. package/dist/setup.mjs +0 -4
  98. package/dist/testing.d.mts +0 -2
  99. package/dist/testing.mjs +0 -3
  100. package/src/client-api.ts +0 -13
  101. package/src/config.ts +0 -3
  102. package/src/server-utils.ts +0 -3
  103. package/src/setup-api.ts +0 -5
  104. package/src/setup.ts +0 -3
  105. package/src/testing.ts +0 -1
  106. /package/dist/{presets → exports/presets}/auto.d.mts +0 -0
  107. /package/dist/{presets → exports/presets}/manual.d.mts +0 -0
  108. /package/src/{presets → exports/presets}/auto.ts +0 -0
  109. /package/src/{presets → exports/presets}/manual.ts +0 -0
@@ -68,6 +68,25 @@ it('should use custom snapshot key', async () => {
68
68
  })
69
69
  ```
70
70
 
71
+ #### `createMissingBaseline`
72
+
73
+ When `true`, if there is **no baseline image yet**,
74
+ the matcher writes the captured image to the baseline path and the assertion succeeds—without using Vitest's `-u` / `updateSnapshot` mode.
75
+ If a baseline already exists, `createMissingBaseline` does not change comparison behavior.
76
+
77
+ Defaults for **auto** snapshots can come from `vis.setup({ createMissingBaseline: true, auto: ... })`; per-test meta can override with an explicit `createMissingBaseline` value.
78
+
79
+ ```ts
80
+ it('creates page baseline on first run with createMissingBaseline', async () => {
81
+ render(<MyApp />)
82
+
83
+ await page.toMatchImageSnapshot({
84
+ snapshotKey: 'page_first_run',
85
+ createMissingBaseline: true
86
+ })
87
+ })
88
+ ```
89
+
71
90
  ### Failure Threshold
72
91
 
73
92
  #### `failureThreshold` and `failureThresholdType`
@@ -256,7 +275,10 @@ it('should demonstrate all page snapshot options', async () => {
256
275
  timeout: 45000,
257
276
 
258
277
  // Testing utility
259
- expectToFail: false
278
+ expectToFail: false,
279
+
280
+ // First-run baseline (optional)
281
+ createMissingBaseline: false
260
282
  })
261
283
  })
262
284
  ```
@@ -8,6 +8,7 @@ export function prettifyOptions(options: ToMatchImageSnapshotOptions<any> | unde
8
8
  options.timeout ? `timeout: ${options.timeout} ms` : '',
9
9
  `comparisonMethod: ${options.comparisonMethod ?? 'pixel'}`,
10
10
  options.diffOptions ? `diffOptions: ${JSON.stringify(options.diffOptions)}` : '',
11
+ options.createMissingBaseline ? 'createMissingBaseline: true' : '',
11
12
  ]
12
13
  .filter(Boolean)
13
14
  .join('\n ')
@@ -23,7 +23,7 @@ export async function compareImageSnapshot(
23
23
  const resultImage = await toImageData(info.result)
24
24
 
25
25
  if (!info.baseline) {
26
- if (server.config.snapshotOptions.updateSnapshot === 'all') {
26
+ if (server.config.snapshotOptions.updateSnapshot === 'all' || options?.createMissingBaseline === true) {
27
27
  await writeSnapshot(commands, resolve(info.projectRoot, info.baselinePath), resultImage)
28
28
  return
29
29
  }
@@ -45,7 +45,7 @@ export async function compareImageSnapshot(
45
45
  diffImage.data,
46
46
  width,
47
47
  height,
48
- options,
48
+ options as ToMatchImageSnapshotOptions<any>,
49
49
  )
50
50
  if (pass) {
51
51
  if (options?.expectToFail) {
@@ -2,7 +2,7 @@ import dedent from 'dedent'
2
2
  import { extractAutoSnapshotOptions } from '../../auto_snapshots/_extract_auto_snapshot_options.ts'
3
3
  import type { SnapshotMeta } from '../../auto_snapshots/snapshot_meta.ts'
4
4
  import type { SetupVisSuiteCommand } from '../../shared/commands.types.ts'
5
- import type { ComparisonMethod } from '../../shared/types.ts'
5
+ import type { ComparisonMethod, SetupVisOptions } from '../../shared/types.ts'
6
6
  import { shouldTakeSnapshot } from '../snapshot/should_take_snapshot.ts'
7
7
  import { toTaskId } from '../task/task_id.ts'
8
8
  import { ctx } from './_ctx.ts'
@@ -22,6 +22,7 @@ export function autoSnapshotMatcher<GM extends Record<string, any> | unknown = u
22
22
  string,
23
23
  boolean | ((options: SnapshotMeta<C> & M & GM) => Promise<boolean> | Promise<void> | boolean | void)
24
24
  >,
25
+ suiteDefaults?: Pick<SetupVisOptions<GM>, 'createMissingBaseline'> | undefined,
25
26
  ) {
26
27
  return async function matchImageSnapshot() {
27
28
  const test = ctx.getCurrentTest()
@@ -35,9 +36,16 @@ export function autoSnapshotMatcher<GM extends Record<string, any> | unknown = u
35
36
  const theme = themes[themeId]
36
37
  const r = typeof theme === 'function' ? await theme(meta! as any) : theme
37
38
  if (r === false) continue
39
+ const effectiveCreateMissingBaseline =
40
+ meta != null && Object.hasOwn(meta as object, 'createMissingBaseline')
41
+ ? (meta as { createMissingBaseline?: boolean }).createMissingBaseline
42
+ : suiteDefaults?.createMissingBaseline
38
43
  await expect(getSubject(meta?.subject ?? subject)).toMatchImageSnapshot({
39
44
  ...meta,
40
45
  snapshotKey: meta?.snapshotKey ?? themeId,
46
+ ...(effectiveCreateMissingBaseline !== undefined
47
+ ? { createMissingBaseline: effectiveCreateMissingBaseline }
48
+ : {}),
41
49
  })
42
50
  } catch (error) {
43
51
  errors.push([themeId, error as Error])
@@ -47,7 +55,7 @@ export function autoSnapshotMatcher<GM extends Record<string, any> | unknown = u
47
55
  if (errors.length === 1) throw errors[0]![1]
48
56
  const taskId = toTaskId(test!)
49
57
  throw new AggregateError(
50
- errors,
58
+ errors.map(([, e]) => e),
51
59
  dedent`Snapshot \`${taskId}\` mismatched
52
60
 
53
61
  ${errors
@@ -72,4 +72,18 @@ export type VisOptions<M extends ComparisonMethod = 'pixel'> = (M extends 'ssim'
72
72
  * @returns The customized snapshot subpath.
73
73
  */
74
74
  snapshotSubpath?: ((options: { subpath: string }) => string) | undefined
75
+ /**
76
+ * When `true`, shortenspaths that would exceed Windows-safe limits (full path length at
77
+ * least 250, below classic `MAX_PATH` 260, or a final path component longer than 255):
78
+ * - After `snapshotRootDir` and `snapshotSubpath`, if the path
79
+ * would exceed that budget, the **final** subpath segment is shortened to
80
+ * `{firstToken}-{hash12}{ext}` (tersify-style).
81
+ * - When writing a baseline, if the **actual** absolute path for the legacy `taskId-key.png` file would
82
+ * exceed the budget, only the final segment of that relative path is shortened the same way (nested
83
+ * `describe` directories from `taskId` stay as-is so `taskId-*.png` globs still work).
84
+ * `snapshotRootDir` itself is not rewritten.
85
+ *
86
+ * @default false
87
+ */
88
+ shortenLongSnapshotPaths?: boolean | undefined
75
89
  }
@@ -0,0 +1,13 @@
1
+ import '../server/commands/augment.ts'
2
+
3
+ export * from '../auto_snapshots/auto_snapshot_options.ts'
4
+ export * from '../client/actions/has_image_snapshot_action.ts'
5
+ export * from '../client/actions/load_image_snapshot_results_action.ts'
6
+ export * from '../client/actions/match_image_snapshot_action.ts'
7
+ export * from '../client/actions/match_page_image_snapshot_action.ts'
8
+ export * from '../client/expect/expectation_result.ts'
9
+ export type * from '../client/expect/to_match_image_snapshot.types.ts'
10
+ export * from '../client/suite/auto_snapshot_matcher.ts'
11
+ export * from '../client/task/task_id.ts'
12
+ export * from '../shared/constants.ts'
13
+ export type * from '../shared/types.ts'
@@ -0,0 +1,3 @@
1
+ export type * from '../config/types.ts'
2
+ export * from '../config/vis.ts'
3
+ export * from '../shared/trim_common_folder.ts'
@@ -0,0 +1,7 @@
1
+ export { getSnapshotRootDir } from '../server/snapshot_path.ts'
2
+ export {
3
+ resolveLegacySnapshotFileRelativePath,
4
+ resolveSnapshotSubpathWithinLimits,
5
+ } from '../server/snapshot_path_limits.ts'
6
+ export { BASELINE_DIR, DIFF_DIR, SNAPSHOT_ROOT_DIR } from '../shared/constants.ts'
7
+ export { trimCommonFolder } from '../shared/trim_common_folder.ts'
@@ -0,0 +1,5 @@
1
+ // `vitest-plugin-vis/setup` provides code needed in `vitest.setup.ts`.
2
+ import '../client/expect/extend.ts'
3
+ import '../client/page/extend.ts'
4
+
5
+ export * from '../setup/create_vis.ts'
@@ -0,0 +1,3 @@
1
+ import './setup-api.ts'
2
+
3
+ export * from '../setup/vis.ts'
@@ -0,0 +1,4 @@
1
+ /** Node / Vitest project tests only — not safe for browser Vitest. */
2
+ export { stubSuite } from '../../server/testing/stubSuite.ts'
3
+ export { getVisOption } from '../../server/vis_options.ts'
4
+ export * from '../../testing/constants.ts'
@@ -0,0 +1 @@
1
+ export * from '../testing/constants.ts'
@@ -28,7 +28,7 @@ export const prepareImageSnapshotComparison: ExtendedBrowserCommand<
28
28
  ...info,
29
29
  projectRoot,
30
30
  result: subject,
31
- }
31
+ } as ImageSnapshotComparisonInfo
32
32
  }
33
33
 
34
34
  const resultBuffer = await takeSnapshot(context, projectRoot, info.resultPath, subject, options)
@@ -37,5 +37,5 @@ export const prepareImageSnapshotComparison: ExtendedBrowserCommand<
37
37
  projectRoot,
38
38
  baseline: baselineBuffer?.toString('base64'),
39
39
  result: resultBuffer.toString('base64'),
40
- }
40
+ } as ImageSnapshotComparisonInfo
41
41
  }
@@ -1,5 +1,6 @@
1
1
  import { resolve } from 'pathe'
2
2
  import type { PreparePageImageSnapshotComparisonCommand } from '../../shared/commands.types.ts'
3
+ import type { ImageSnapshotComparisonInfo } from '../../shared/types.ts'
3
4
  import { file } from '../externals/file.ts'
4
5
  import { getProjectRoot } from '../project.ts'
5
6
  import { takePageSnapshot } from '../snapshot.ts'
@@ -24,5 +25,5 @@ export const preparePageImageSnapshotComparison: ExtendedBrowserCommand<
24
25
  projectRoot,
25
26
  baseline: baselineBuffer?.toString('base64'),
26
27
  result: resultBuffer.toString('base64'),
27
- }
28
+ } as ImageSnapshotComparisonInfo
28
29
  }
@@ -0,0 +1,135 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { basename, dirname, extname, join, resolve } from 'pathe'
3
+
4
+ /** Classic Win32 `MAX_PATH` includes NUL; tools often treat ~259 as usable. */
5
+ export const WIN_MAX_PATH = 260
6
+
7
+ /**
8
+ * Budget under {@link WIN_MAX_PATH} to leave margin for tooling and long `projectRoot` prefixes.
9
+ */
10
+ export const WIN_SNAPSHOT_PATH_BUDGET = 250
11
+
12
+ /** NTFS maximum length of a single path component (directory or file name). */
13
+ export const NTFS_MAX_FILENAME = 255
14
+
15
+ /** Max length of the readable prefix taken from the original basename (tersify-style). */
16
+ const SNAPSHOT_SUBPATH_PREFIX_MAX_LEN = 24
17
+
18
+ /**
19
+ * Minimal probe file under the snapshot folder — detects when the directory prefix alone is too long.
20
+ */
21
+ const SNAPSHOT_SUBPATH_PROBE_MIN_BASENAME = '0.png'
22
+
23
+ export function absoluteSnapshotFilePath(projectRoot: string, baselineDir: string, snapshotFilename: string): string {
24
+ return resolve(projectRoot, join(baselineDir, snapshotFilename))
25
+ }
26
+
27
+ /**
28
+ * @returns true when the legacy absolute path should be shortened for Windows-safe tooling.
29
+ */
30
+ export function legacySnapshotPathExceedsLimit(absolutePath: string): boolean {
31
+ if (absolutePath.length >= WIN_SNAPSHOT_PATH_BUDGET) return true
32
+ if (basename(absolutePath).length > NTFS_MAX_FILENAME) return true
33
+ return false
34
+ }
35
+
36
+ /**
37
+ * First readable token of a snapshot basename stem (before the first `.`), similar in spirit to
38
+ * [tersify](https://github.com/unional/tersify) terse labels — keeps a short human hint, not the whole name.
39
+ */
40
+ function snapshotBasenameTersifyPrefix(stem: string): string {
41
+ if (!stem) return 'snap'
42
+ const dot = stem.indexOf('.')
43
+ const first = dot === -1 ? stem : stem.slice(0, dot)
44
+ const cleaned = first.replace(/[^\w-]/g, '')
45
+ return (cleaned || 'snap').slice(0, SNAPSHOT_SUBPATH_PREFIX_MAX_LEN)
46
+ }
47
+
48
+ /**
49
+ * Keeps all directory segments of {@link rawSnapshotSubpath}; only the final filename is replaced with
50
+ * `{prefix}-{hash12}{ext}` where `prefix` comes from the first segment of the original stem (e.g. `Button.stories.ts` → `Button`).
51
+ */
52
+ export function formatCondensedSnapshotSubpath(rawSnapshotSubpath: string): string {
53
+ const norm = rawSnapshotSubpath.replace(/\\/g, '/')
54
+ const dir = dirname(norm)
55
+ const file = basename(norm)
56
+ const ext = extname(file)
57
+ const stem = ext ? file.slice(0, -ext.length) : file
58
+ const prefix = snapshotBasenameTersifyPrefix(stem)
59
+ const h = createHash('sha256').update(norm).digest('hex').slice(0, 12)
60
+ const shortFile = `${prefix}-${h}${ext}`
61
+ if (dir === '.' || dir === '') return shortFile
62
+ return join(dir, shortFile)
63
+ }
64
+
65
+ /**
66
+ * `snapshotBaselineRootAbs` is the absolute `__baselines__` root; with `rawSnapshotSubpath` this yields the
67
+ * test-module snapshot directory. Only checks whether that layout can host a tiny baseline file; long
68
+ * `taskId-key.png` paths are handled with {@link resolveLegacySnapshotFileRelativePath} using real names.
69
+ */
70
+ function rawSnapshotSubpathExceedsSafePathBudget(snapshotBaselineRootAbs: string, rawSnapshotSubpath: string): boolean {
71
+ const dirAbs = resolve(snapshotBaselineRootAbs, rawSnapshotSubpath)
72
+
73
+ if (legacySnapshotPathExceedsLimit(resolve(dirAbs, SNAPSHOT_SUBPATH_PROBE_MIN_BASENAME))) return true
74
+
75
+ for (const segment of rawSnapshotSubpath.split(/[/\\]/)) {
76
+ if (segment.length > NTFS_MAX_FILENAME) return true
77
+ }
78
+
79
+ return false
80
+ }
81
+
82
+ /**
83
+ * Returns {@link rawSnapshotSubpath} or a tersified final segment when `shortenLongSnapshotPaths` is on and
84
+ * `snapshotBaselineRootAbs` + subpath + a minimal `0.png` would exceed {@link legacySnapshotPathExceedsLimit},
85
+ * or a subpath segment exceeds NTFS length. Long snapshot **filenames** (`taskId-key.png`) are shortened
86
+ * separately via {@link resolveLegacySnapshotFileRelativePath}. Baseline, result, and diff trees share the
87
+ * same resolved subpath via {@link getTaskSubpath}.
88
+ */
89
+ export function resolveSnapshotSubpathWithinLimits(params: {
90
+ snapshotBaselineRootAbs: string
91
+ rawSnapshotSubpath: string
92
+ shortenLongSnapshotPaths: boolean
93
+ }): string {
94
+ const { snapshotBaselineRootAbs, rawSnapshotSubpath, shortenLongSnapshotPaths } = params
95
+ if (!shortenLongSnapshotPaths) return rawSnapshotSubpath
96
+
97
+ if (!rawSnapshotSubpathExceedsSafePathBudget(snapshotBaselineRootAbs, rawSnapshotSubpath)) {
98
+ return rawSnapshotSubpath
99
+ }
100
+
101
+ return formatCondensedSnapshotSubpath(rawSnapshotSubpath)
102
+ }
103
+
104
+ /**
105
+ * When `shortenLongSnapshotPaths` is on and the legacy `{@link getLegacySnapshotFilename}` path exceeds
106
+ * {@link legacySnapshotPathExceedsLimit}, rewrites only the final path segment (after the last `/`) like
107
+ * {@link formatCondensedSnapshotSubpath}, so nested `taskId` directories stay intact and `…-*.png` globs keep working.
108
+ */
109
+ export function resolveLegacySnapshotFileRelativePath(params: {
110
+ projectRoot: string
111
+ baselineDir: string
112
+ legacySnapshotFilename: string
113
+ shortenLongSnapshotPaths: boolean
114
+ }): string {
115
+ const { projectRoot, baselineDir, legacySnapshotFilename, shortenLongSnapshotPaths } = params
116
+ if (!shortenLongSnapshotPaths) return legacySnapshotFilename
117
+
118
+ const norm = legacySnapshotFilename.replace(/\\/g, '/')
119
+ const abs = absoluteSnapshotFilePath(projectRoot, baselineDir, norm)
120
+ if (!legacySnapshotPathExceedsLimit(abs)) return legacySnapshotFilename
121
+
122
+ return formatCondensedSnapshotSubpath(norm)
123
+ }
124
+
125
+ export function getLegacySnapshotFilename(params: {
126
+ taskId: string
127
+ explicitSnapshotKey: string | undefined
128
+ defaultSnapshotKey: string | undefined
129
+ taskCount: number
130
+ }): string {
131
+ const key =
132
+ params.explicitSnapshotKey ??
133
+ (typeof params.defaultSnapshotKey === 'string' ? params.defaultSnapshotKey : String(params.taskCount))
134
+ return `${params.taskId}-${key}.png`
135
+ }
@@ -1,8 +1,9 @@
1
- import { join, relative } from 'pathe'
1
+ import { join, relative, resolve } from 'pathe'
2
2
  import type { VisOptions } from '../config/types.ts'
3
3
  import { BASELINE_DIR, DIFF_DIR, RESULT_DIR } from '../shared/constants.ts'
4
4
  import { getProjectName, getProjectRoot } from './project.ts'
5
5
  import { getSnapshotSubpath, resolveSnapshotRootDir } from './snapshot_path.ts'
6
+ import { resolveSnapshotSubpathWithinLimits } from './snapshot_path_limits.ts'
6
7
  import { getVisOption } from './vis_options.ts'
7
8
  import { deps } from './vis_server_context.deps.ts'
8
9
  import type { ExtendedBrowserCommandContext, VisSuite, VisSuites } from './vis_server_context.types.ts'
@@ -75,7 +76,11 @@ async function createSuite(
75
76
  return state
76
77
  }
77
78
 
78
- export function createModule(state: VisSuite, testPath: string, options: Pick<VisOptions, 'snapshotSubpath'>) {
79
+ export function createModule(
80
+ state: VisSuite,
81
+ testPath: string,
82
+ options: Pick<VisOptions, 'snapshotSubpath' | 'shortenLongSnapshotPaths'>,
83
+ ) {
79
84
  const taskSubpath = getTaskSubpath(state, testPath, options)
80
85
  return {
81
86
  taskSubpath,
@@ -86,8 +91,18 @@ export function createModule(state: VisSuite, testPath: string, options: Pick<Vi
86
91
  }
87
92
  }
88
93
 
89
- export function getTaskSubpath(state: VisSuite, testPath: string, options: Pick<VisOptions, 'snapshotSubpath'>) {
90
- return getSnapshotSubpath(relative(state.projectRoot, testPath), options)
94
+ export function getTaskSubpath(
95
+ state: VisSuite,
96
+ testPath: string,
97
+ options: Pick<VisOptions, 'snapshotSubpath' | 'shortenLongSnapshotPaths'>,
98
+ ) {
99
+ const raw = getSnapshotSubpath(relative(state.projectRoot, testPath), options)
100
+ const snapshotBaselineRootAbs = resolve(state.projectRoot, state.snapshotBaselineDir)
101
+ return resolveSnapshotSubpathWithinLimits({
102
+ snapshotBaselineRootAbs,
103
+ rawSnapshotSubpath: raw,
104
+ shortenLongSnapshotPaths: options.shortenLongSnapshotPaths === true,
105
+ })
91
106
  }
92
107
 
93
108
  export function getSuite(context: ExtendedBrowserCommandContext) {
@@ -8,6 +8,13 @@ const DEFAULT_PROJECT_NAME = '__default'
8
8
 
9
9
  const visOptions: Record<string, VisOptions<any> | undefined> = {}
10
10
 
11
+ /** Clears stored options (for tests that share this module). */
12
+ export function clearVisOptionsRegistryForTesting() {
13
+ for (const key of Object.keys(visOptions)) {
14
+ delete visOptions[key]
15
+ }
16
+ }
17
+
11
18
  export function setVisOption(
12
19
  userConfig: { test?: { name?: TestUserConfig['name'] | undefined } },
13
20
  options: VisOptions<any> | undefined,
@@ -22,5 +29,7 @@ export function setVisOption(
22
29
 
23
30
  export function getVisOption(context: ExtendedBrowserCommandContext) {
24
31
  const id = getProjectName(context) || DEFAULT_PROJECT_NAME
25
- return visOptions[id] ?? {}
32
+ const base = visOptions[DEFAULT_PROJECT_NAME] ?? {}
33
+ const override = visOptions[id] ?? {}
34
+ return { ...base, ...override }
26
35
  }
@@ -1,8 +1,13 @@
1
1
  import { basename, join, resolve } from 'pathe'
2
2
  import { pick } from 'type-plus'
3
- import type { ImageSnapshotKeyOptions } from '../client-api.ts'
3
+ import type { ImageSnapshotKeyOptions } from '../exports/client-api.ts'
4
4
  import type { ImageSnapshotResult } from '../shared/commands.types.ts'
5
5
  import { file } from './externals/file.ts'
6
+ import {
7
+ absoluteSnapshotFilePath,
8
+ getLegacySnapshotFilename,
9
+ resolveLegacySnapshotFileRelativePath,
10
+ } from './snapshot_path_limits.ts'
6
11
  import { getSuite, getTaskSubpath } from './suite.ts'
7
12
  import { getVisOption } from './vis_options.ts'
8
13
  import type { ExtendedBrowserCommandContext } from './vis_server_context.types.ts'
@@ -11,16 +16,23 @@ export function createVisServerContext() {
11
16
  const context = {
12
17
  async getSnapshotResults(browserContext: ExtendedBrowserCommandContext, taskId: string) {
13
18
  const suiteInfo = await context.getSuiteInfo(browserContext, taskId)
14
- const baselines = await file.glob(join(suiteInfo.projectRoot, suiteInfo.baselineDir, `${suiteInfo.taskId}-*.png`))
15
- const results = await file.glob(join(suiteInfo.projectRoot, suiteInfo.resultDir, `${suiteInfo.taskId}-*.png`))
16
- const diffs = await file.glob(join(suiteInfo.projectRoot, suiteInfo.diffDir, `${suiteInfo.taskId}-*.png`))
19
+ const base = resolve(suiteInfo.projectRoot, suiteInfo.baselineDir)
20
+
21
+ const legacyPattern = join(base, `${suiteInfo.taskId}-*.png`)
22
+ const baselines = await file.glob(legacyPattern)
23
+
24
+ const resultPattern = join(resolve(suiteInfo.projectRoot, suiteInfo.resultDir), `${suiteInfo.taskId}-*.png`)
25
+ const results = await file.glob(resultPattern)
26
+
27
+ const diffPattern = join(resolve(suiteInfo.projectRoot, suiteInfo.diffDir), `${suiteInfo.taskId}-*.png`)
28
+ const diffs = await file.glob(diffPattern)
17
29
 
18
30
  const r: ImageSnapshotResult[] = []
19
31
  await Promise.all(
20
32
  baselines.map(async (baselinePath) => {
21
33
  const filename = basename(baselinePath)
22
- const resultPath = results.find((r) => r.endsWith(filename))
23
- const diffPath = diffs.find((d) => d.endsWith(filename))
34
+ const resultPath = results.find((p) => p.endsWith(filename))
35
+ const diffPath = diffs.find((p) => p.endsWith(filename))
24
36
 
25
37
  const baselineBuffer = await file.tryReadFile(baselinePath)
26
38
  const resultBuffer = await file.tryReadFile(resultPath)
@@ -45,7 +57,7 @@ export function createVisServerContext() {
45
57
 
46
58
  const { baselineDir, resultDir, diffDir, task } = suiteInfo
47
59
 
48
- task.count = task.count + 1
60
+ task.count += 1
49
61
  const baselinePath = join(baselineDir, snapshotFilename)
50
62
  const resultPath = join(resultDir, snapshotFilename)
51
63
  const diffPath = join(diffDir, snapshotFilename)
@@ -74,22 +86,40 @@ export function createVisServerContext() {
74
86
  snapshotKey: string | undefined,
75
87
  ) {
76
88
  const info = await context.getSuiteInfo(browserContext, taskId)
77
-
78
- return file.existFile(
79
- resolve(info.projectRoot, info.baselineDir, context.getSnapshotFilename(browserContext, info, snapshotKey)),
80
- )
89
+ const visOptions = getVisOption(browserContext)
90
+ const legacyFilename = getLegacySnapshotFilename({
91
+ taskId: info.taskId,
92
+ explicitSnapshotKey: snapshotKey,
93
+ defaultSnapshotKey: typeof visOptions.snapshotKey === 'string' ? visOptions.snapshotKey : undefined,
94
+ taskCount: info.task.count,
95
+ })
96
+ const filename = resolveLegacySnapshotFileRelativePath({
97
+ projectRoot: info.projectRoot,
98
+ baselineDir: info.baselineDir,
99
+ legacySnapshotFilename: legacyFilename,
100
+ shortenLongSnapshotPaths: visOptions.shortenLongSnapshotPaths === true,
101
+ })
102
+ const legacyAbs = absoluteSnapshotFilePath(info.projectRoot, info.baselineDir, filename)
103
+ return file.existFile(legacyAbs)
81
104
  },
82
105
  getSnapshotFilename(
83
106
  browserContext: ExtendedBrowserCommandContext,
84
- info: { taskId: string; task: { count: number } },
107
+ info: { taskId: string; task: { count: number }; suiteId: string; baselineDir: string; projectRoot: string },
85
108
  snapshotKey: string | undefined,
86
109
  ) {
87
- if (snapshotKey) return `${info.taskId}-${snapshotKey}.png`
88
110
  const visOptions = getVisOption(browserContext)
89
- if (typeof visOptions.snapshotKey === 'string') {
90
- return `${info.taskId}-${visOptions.snapshotKey}.png`
91
- }
92
- return `${info.taskId}-${info.task.count}.png`
111
+ const legacyFilename = getLegacySnapshotFilename({
112
+ taskId: info.taskId,
113
+ explicitSnapshotKey: snapshotKey,
114
+ defaultSnapshotKey: typeof visOptions.snapshotKey === 'string' ? visOptions.snapshotKey : undefined,
115
+ taskCount: info.task.count,
116
+ })
117
+ return resolveLegacySnapshotFileRelativePath({
118
+ projectRoot: info.projectRoot,
119
+ baselineDir: info.baselineDir,
120
+ legacySnapshotFilename: legacyFilename,
121
+ shortenLongSnapshotPaths: visOptions.shortenLongSnapshotPaths === true,
122
+ })
93
123
  },
94
124
  async getSuiteInfo(browserContext: ExtendedBrowserCommandContext, taskId: string) {
95
125
  const projectState = await getSuite(browserContext)
@@ -44,21 +44,22 @@ export function createVis<GM extends Record<string, any> | unknown = unknown>(co
44
44
 
45
45
  const vis: VisClientConfigurator<GM> = {
46
46
  setup(options) {
47
- if (!options || options.auto === false) {
48
- beforeAll(matcher.setup)
49
- } else {
50
- beforeAll(async () => {
51
- await matcher.setup()
52
- setAutoSnapshotOptions(true)
53
- })
54
- }
47
+ const suiteDefaults =
48
+ options?.createMissingBaseline !== undefined
49
+ ? { createMissingBaseline: options.createMissingBaseline }
50
+ : undefined
51
+
52
+ beforeAll(async () => {
53
+ await matcher.setup()
54
+ setAutoSnapshotOptions(!!options?.auto)
55
+ })
55
56
 
56
57
  if (typeof options?.auto === 'function') {
57
- afterEach(matcher.createMatcher({ auto: options.auto }))
58
+ afterEach(matcher.createMatcher({ auto: options.auto }, suiteDefaults))
58
59
  } else if (typeof options?.auto === 'object') {
59
- afterEach(matcher.createMatcher(options.auto))
60
+ afterEach(matcher.createMatcher(options.auto, suiteDefaults))
60
61
  } else {
61
- afterEach(matcher.createMatcher({ async auto() {} }))
62
+ afterEach(matcher.createMatcher({ async auto() {} }, suiteDefaults))
62
63
  }
63
64
  },
64
65
  }
@@ -94,6 +94,15 @@ export type ToMatchImageSnapshotOptions<M extends ComparisonMethod = 'pixel'> =
94
94
  * If it passes, it will throw an error with details.
95
95
  */
96
96
  expectToFail?: boolean | undefined
97
+ /**
98
+ * Create a baseline image when none exists on disk.
99
+ *
100
+ * If `true`, the captured image is written to the baseline path and the assertion succeeds,
101
+ * without Vitest `-u` / `updateSnapshot: 'all'`.
102
+ *
103
+ * @default false
104
+ */
105
+ createMissingBaseline?: boolean | undefined
97
106
  }
98
107
 
99
108
  export interface PageImageSnapshotOptions {
@@ -145,7 +154,12 @@ export type ImageSnapshotComparisonInfo = {
145
154
 
146
155
  export type MatchImageSnapshotOptions = ImageSnapshotTimeoutOptions &
147
156
  ImageSnapshotCompareOptions<any> &
148
- ImageSnapshotKeyOptions
157
+ ImageSnapshotKeyOptions & {
158
+ /**
159
+ * When `true`, write a missing baseline on first run (see {@link ToMatchImageSnapshotOptions} `createMissingBaseline`).
160
+ */
161
+ createMissingBaseline?: boolean | undefined
162
+ }
149
163
 
150
164
  export type SetupVisOptions<GM extends Record<string, any> | unknown = unknown> = {
151
165
  auto?:
@@ -160,4 +174,10 @@ export type SetupVisOptions<GM extends Record<string, any> | unknown = unknown>
160
174
  options: SnapshotMeta<C> & M & GM,
161
175
  ) => Promise<boolean> | Promise<void> | boolean | void)
162
176
  >
177
+ /**
178
+ * Default {@link ToMatchImageSnapshotOptions#createMissingBaseline} for auto snapshots.
179
+ *
180
+ * @default false
181
+ */
182
+ createMissingBaseline?: boolean | undefined
163
183
  }
@@ -1,11 +0,0 @@
1
- import { BASELINE_DIR, DIFF_DIR, NAME, RESULT_DIR, SNAPSHOT_ROOT_DIR } from "./shared/constants.mjs";
2
- import { setAutoSnapshotOptions } from "./auto_snapshots/auto_snapshot_options.mjs";
3
- import { hasImageSnapshotAction } from "./client/actions/has_image_snapshot_action.mjs";
4
- import { loadImageSnapshotResultsAction } from "./client/actions/load_image_snapshot_results_action.mjs";
5
- import { matchImageSnapshotAction } from "./client/actions/match_image_snapshot_action.mjs";
6
- import { matchPageImageSnapshotAction } from "./client/actions/match_page_image_snapshot_action.mjs";
7
- import { success } from "./client/expect/expectation_result.mjs";
8
- import { toTaskId } from "./client/task/task_id.mjs";
9
- import { autoSnapshotMatcher } from "./client/suite/auto_snapshot_matcher.mjs";
10
-
11
- export { BASELINE_DIR, DIFF_DIR, NAME, RESULT_DIR, SNAPSHOT_ROOT_DIR, autoSnapshotMatcher, hasImageSnapshotAction, loadImageSnapshotResultsAction, matchImageSnapshotAction, matchPageImageSnapshotAction, setAutoSnapshotOptions, success, toTaskId };
package/dist/config.d.mts DELETED
@@ -1,5 +0,0 @@
1
- import { ComparisonMethod, PixelComparisonOptions, PixelDiffOptions, SsimComparisonOptions, SsimDiffOptions } from "./shared/types.mjs";
2
- import { VisOptions } from "./config/types.mjs";
3
- import { vis } from "./config/vis.mjs";
4
- import { trimCommonFolder } from "./shared/trim_common_folder.mjs";
5
- export { ComparisonMethod, PixelComparisonOptions, PixelDiffOptions, SsimComparisonOptions, SsimDiffOptions, VisOptions, trimCommonFolder, vis };
package/dist/config.mjs DELETED
@@ -1,4 +0,0 @@
1
- import { trimCommonFolder } from "./shared/trim_common_folder.mjs";
2
- import { vis } from "./config/vis.mjs";
3
-
4
- export { trimCommonFolder, vis };
@@ -1,9 +0,0 @@
1
- import { vis } from "../setup/vis.mjs";
2
- import "../setup.mjs";
3
-
4
- //#region src/presets/auto.ts
5
- vis.setup({ auto: true });
6
-
7
- //#endregion
8
- export { };
9
- //# sourceMappingURL=auto.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auto.mjs","names":[],"sources":["../../src/presets/auto.ts"],"sourcesContent":["import { vis } from '../setup.ts'\n\nvis.setup({ auto: true })\n"],"mappings":";;;;AAEA,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC"}
@@ -1,9 +0,0 @@
1
- import { vis } from "../setup/vis.mjs";
2
- import "../setup.mjs";
3
-
4
- //#region src/presets/manual.ts
5
- vis.setup({ auto: false });
6
-
7
- //#endregion
8
- export { };
9
- //# sourceMappingURL=manual.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"manual.mjs","names":[],"sources":["../../src/presets/manual.ts"],"sourcesContent":["import { vis } from '../setup.ts'\n\nvis.setup({ auto: false })\n"],"mappings":";;;;AAEA,IAAI,MAAM,EAAE,MAAM,OAAO,CAAC"}
@@ -1,4 +0,0 @@
1
- import { BASELINE_DIR, DIFF_DIR, NAME, RESULT_DIR, SNAPSHOT_ROOT_DIR } from "./shared/constants.mjs";
2
- import { trimCommonFolder } from "./shared/trim_common_folder.mjs";
3
- import { getSnapshotRootDir } from "./server/snapshot_path.mjs";
4
- export { BASELINE_DIR, DIFF_DIR, NAME, RESULT_DIR, SNAPSHOT_ROOT_DIR, getSnapshotRootDir, trimCommonFolder };