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.
- package/dist/auto_snapshots/_extract_auto_snapshot_options.mjs +1 -1
- package/dist/auto_snapshots/_extract_auto_snapshot_options.mjs.map +1 -1
- package/dist/client/external/vitest/vitest_suite_proxy.d.mts +0 -1
- package/dist/client/snapshot/compare_image_snapshot.logic.mjs +2 -1
- package/dist/client/snapshot/compare_image_snapshot.logic.mjs.map +1 -1
- package/dist/client/snapshot/compare_image_snapshot.mjs +1 -1
- package/dist/client/snapshot/compare_image_snapshot.mjs.map +1 -1
- package/dist/client/suite/auto_snapshot_matcher.d.mts +2 -2
- package/dist/client/suite/auto_snapshot_matcher.d.mts.map +1 -1
- package/dist/client/suite/auto_snapshot_matcher.mjs +5 -3
- package/dist/client/suite/auto_snapshot_matcher.mjs.map +1 -1
- package/dist/config/types.d.mts +14 -0
- package/dist/{client-api.d.mts → exports/client-api.d.mts} +12 -12
- package/dist/exports/client-api.mjs +11 -0
- package/dist/exports/config.d.mts +5 -0
- package/dist/exports/config.mjs +4 -0
- package/dist/exports/presets/auto.mjs +9 -0
- package/dist/exports/presets/auto.mjs.map +1 -0
- package/dist/exports/presets/manual.mjs +9 -0
- package/dist/exports/presets/manual.mjs.map +1 -0
- package/dist/exports/server-utils.d.mts +5 -0
- package/dist/exports/server-utils.mjs +6 -0
- package/dist/exports/setup-api.d.mts +4 -0
- package/dist/exports/setup-api.mjs +5 -0
- package/dist/exports/setup.d.mts +3 -0
- package/dist/exports/setup.mjs +4 -0
- package/dist/exports/testing/node.d.mts +4 -0
- package/dist/exports/testing/node.mjs +5 -0
- package/dist/exports/testing.d.mts +2 -0
- package/dist/exports/testing.mjs +3 -0
- package/dist/server/commands/prepare_image_snapshot_comparison.mjs.map +1 -1
- package/dist/server/commands/prepare_page_image_snapshot_comparison.mjs.map +1 -1
- package/dist/server/snapshot_path.d.mts +0 -1
- package/dist/server/snapshot_path.d.mts.map +1 -1
- package/dist/server/snapshot_path_limits.d.mts +28 -0
- package/dist/server/snapshot_path_limits.d.mts.map +1 -0
- package/dist/server/snapshot_path_limits.mjs +92 -0
- package/dist/server/snapshot_path_limits.mjs.map +1 -0
- package/dist/server/suite.mjs +8 -2
- package/dist/server/suite.mjs.map +1 -1
- package/dist/server/testing/stubBrowserCommandContext.mjs +9 -0
- package/dist/server/testing/stubBrowserCommandContext.mjs.map +1 -0
- package/dist/server/testing/stubSuite.d.mts +17 -0
- package/dist/server/testing/stubSuite.d.mts.map +1 -0
- package/dist/server/testing/stubSuite.mjs +23 -0
- package/dist/server/testing/stubSuite.mjs.map +1 -0
- package/dist/server/testing/stubUserConfig.mjs +8 -0
- package/dist/server/testing/stubUserConfig.mjs.map +1 -0
- package/dist/server/vis_options.d.mts +120 -0
- package/dist/server/vis_options.d.mts.map +1 -0
- package/dist/server/vis_options.mjs +7 -1
- package/dist/server/vis_options.mjs.map +1 -1
- package/dist/server/vis_server_context.logic.mjs +37 -10
- package/dist/server/vis_server_context.logic.mjs.map +1 -1
- package/dist/server/vis_server_context.types.d.mts +32 -1
- package/dist/server/vis_server_context.types.d.mts.map +1 -0
- package/dist/setup/create_vis.mjs +6 -6
- package/dist/setup/create_vis.mjs.map +1 -1
- package/dist/shared/types.d.mts +21 -1
- package/dist/shared/types.d.mts.map +1 -1
- package/package.json +13 -36
- package/readme.md +6 -0
- package/src/auto_snapshots/_extract_auto_snapshot_options.ts +1 -1
- package/src/client/expect/to_match_image_snapshot.mdx +24 -1
- package/src/client/page/to_match_image_snapshot.mdx +23 -1
- package/src/client/snapshot/compare_image_snapshot.logic.ts +1 -0
- package/src/client/snapshot/compare_image_snapshot.ts +2 -2
- package/src/client/suite/auto_snapshot_matcher.ts +10 -2
- package/src/config/types.ts +14 -0
- package/src/exports/client-api.ts +13 -0
- package/src/exports/config.ts +3 -0
- package/src/exports/server-utils.ts +7 -0
- package/src/exports/setup-api.ts +5 -0
- package/src/exports/setup.ts +3 -0
- package/src/exports/testing/node.ts +4 -0
- package/src/exports/testing.ts +1 -0
- package/src/server/commands/prepare_image_snapshot_comparison.ts +2 -2
- package/src/server/commands/prepare_page_image_snapshot_comparison.ts +2 -1
- package/src/server/snapshot_path_limits.ts +135 -0
- package/src/server/suite.ts +19 -4
- package/src/server/vis_options.ts +10 -1
- package/src/server/vis_server_context.logic.ts +47 -17
- package/src/setup/create_vis.ts +12 -11
- package/src/shared/types.ts +21 -1
- package/dist/client-api.mjs +0 -11
- package/dist/config.d.mts +0 -5
- package/dist/config.mjs +0 -4
- package/dist/presets/auto.mjs +0 -9
- package/dist/presets/auto.mjs.map +0 -1
- package/dist/presets/manual.mjs +0 -9
- package/dist/presets/manual.mjs.map +0 -1
- package/dist/server-utils.d.mts +0 -4
- package/dist/server-utils.mjs +0 -5
- package/dist/setup-api.d.mts +0 -4
- package/dist/setup-api.mjs +0 -5
- package/dist/setup.d.mts +0 -3
- package/dist/setup.mjs +0 -4
- package/dist/testing.d.mts +0 -2
- package/dist/testing.mjs +0 -3
- package/src/client-api.ts +0 -13
- package/src/config.ts +0 -3
- package/src/server-utils.ts +0 -3
- package/src/setup-api.ts +0 -5
- package/src/setup.ts +0 -3
- package/src/testing.ts +0 -1
- /package/dist/{presets → exports/presets}/auto.d.mts +0 -0
- /package/dist/{presets → exports/presets}/manual.d.mts +0 -0
- /package/src/{presets → exports/presets}/auto.ts +0 -0
- /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
|
package/src/config/types.ts
CHANGED
|
@@ -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,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 @@
|
|
|
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
|
+
}
|
package/src/server/suite.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
const
|
|
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((
|
|
23
|
-
const diffPath = diffs.find((
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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)
|
package/src/setup/create_vis.ts
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
}
|
package/src/shared/types.ts
CHANGED
|
@@ -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
|
}
|
package/dist/client-api.mjs
DELETED
|
@@ -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
package/dist/presets/auto.mjs
DELETED
|
@@ -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"}
|
package/dist/presets/manual.mjs
DELETED
|
@@ -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"}
|
package/dist/server-utils.d.mts
DELETED
|
@@ -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 };
|