vitest-plugin-vis 1.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 (204) hide show
  1. package/esm/client/ctx.d.ts +6 -0
  2. package/esm/client/ctx.d.ts.map +1 -0
  3. package/esm/client/ctx.js +7 -0
  4. package/esm/client/expect/expectation_result.d.ts +3 -0
  5. package/esm/client/expect/expectation_result.d.ts.map +1 -0
  6. package/esm/client/expect/expectation_result.js +5 -0
  7. package/esm/client/expect/extend.d.ts +8 -0
  8. package/esm/client/expect/extend.d.ts.map +1 -0
  9. package/esm/client/expect/extend.js +3 -0
  10. package/esm/client/expect/to_match_image_snapshot.d.ts +9 -0
  11. package/esm/client/expect/to_match_image_snapshot.d.ts.map +1 -0
  12. package/esm/client/expect/to_match_image_snapshot.js +32 -0
  13. package/esm/client/expect/to_match_image_snapshot.logic.d.ts +2 -0
  14. package/esm/client/expect/to_match_image_snapshot.logic.d.ts.map +1 -0
  15. package/esm/client/expect/to_match_image_snapshot.logic.js +11 -0
  16. package/esm/client/expect/to_match_image_snapshot.types.d.ts +7 -0
  17. package/esm/client/expect/to_match_image_snapshot.types.d.ts.map +1 -0
  18. package/esm/client/expect/to_match_image_snapshot.types.js +1 -0
  19. package/esm/client/page/extend.d.ts +6 -0
  20. package/esm/client/page/extend.d.ts.map +1 -0
  21. package/esm/client/page/extend.js +3 -0
  22. package/esm/client/page/has_image_snapshot.d.ts +10 -0
  23. package/esm/client/page/has_image_snapshot.d.ts.map +1 -0
  24. package/esm/client/page/has_image_snapshot.js +23 -0
  25. package/esm/client/selector.d.ts +9 -0
  26. package/esm/client/selector.d.ts.map +1 -0
  27. package/esm/client/selector.js +104 -0
  28. package/esm/client/should_take_snapshot.d.ts +10 -0
  29. package/esm/client/should_take_snapshot.d.ts.map +1 -0
  30. package/esm/client/should_take_snapshot.js +13 -0
  31. package/esm/client/snapshot_options.d.ts +34 -0
  32. package/esm/client/snapshot_options.d.ts.map +1 -0
  33. package/esm/client/snapshot_options.internal.d.ts +3 -0
  34. package/esm/client/snapshot_options.internal.d.ts.map +1 -0
  35. package/esm/client/snapshot_options.internal.js +20 -0
  36. package/esm/client/snapshot_options.js +16 -0
  37. package/esm/client/task_id.d.ts +7 -0
  38. package/esm/client/task_id.d.ts.map +1 -0
  39. package/esm/client/task_id.js +10 -0
  40. package/esm/client.d.ts +8 -0
  41. package/esm/client.d.ts.map +1 -0
  42. package/esm/client.js +5 -0
  43. package/esm/commands.d.ts +5 -0
  44. package/esm/commands.d.ts.map +1 -0
  45. package/esm/commands.js +4 -0
  46. package/esm/config/types.d.ts +35 -0
  47. package/esm/config/types.d.ts.map +1 -0
  48. package/esm/config/types.js +1 -0
  49. package/esm/config/vis.d.ts +25 -0
  50. package/esm/config/vis.d.ts.map +1 -0
  51. package/esm/config/vis.js +27 -0
  52. package/esm/config.d.ts +4 -0
  53. package/esm/config.d.ts.map +1 -0
  54. package/esm/config.js +2 -0
  55. package/esm/index.d.ts +2 -0
  56. package/esm/index.d.ts.map +1 -0
  57. package/esm/index.js +2 -0
  58. package/esm/presets/auto.d.ts +2 -0
  59. package/esm/presets/auto.d.ts.map +1 -0
  60. package/esm/presets/auto.js +3 -0
  61. package/esm/presets/manual.d.ts +2 -0
  62. package/esm/presets/manual.d.ts.map +1 -0
  63. package/esm/presets/manual.js +3 -0
  64. package/esm/server/browser_provider/browser_api.d.ts +3 -0
  65. package/esm/server/browser_provider/browser_api.d.ts.map +1 -0
  66. package/esm/server/browser_provider/browser_api.js +11 -0
  67. package/esm/server/browser_provider/playwright.d.ts +4 -0
  68. package/esm/server/browser_provider/playwright.d.ts.map +1 -0
  69. package/esm/server/browser_provider/playwright.js +13 -0
  70. package/esm/server/browser_provider/types.d.ts +6 -0
  71. package/esm/server/browser_provider/types.d.ts.map +1 -0
  72. package/esm/server/browser_provider/types.js +1 -0
  73. package/esm/server/browser_provider/webdriverio.d.ts +4 -0
  74. package/esm/server/browser_provider/webdriverio.d.ts.map +1 -0
  75. package/esm/server/browser_provider/webdriverio.js +10 -0
  76. package/esm/server/commands/extend.d.ts +15 -0
  77. package/esm/server/commands/extend.d.ts.map +1 -0
  78. package/esm/server/commands/extend.js +10 -0
  79. package/esm/server/commands/has_image_snapshot.d.ts +6 -0
  80. package/esm/server/commands/has_image_snapshot.d.ts.map +1 -0
  81. package/esm/server/commands/has_image_snapshot.js +7 -0
  82. package/esm/server/commands/image_snapshot_next_index.d.ts +9 -0
  83. package/esm/server/commands/image_snapshot_next_index.d.ts.map +1 -0
  84. package/esm/server/commands/image_snapshot_next_index.js +7 -0
  85. package/esm/server/commands/is_ci.d.ts +2 -0
  86. package/esm/server/commands/is_ci.d.ts.map +1 -0
  87. package/esm/server/commands/is_ci.js +6 -0
  88. package/esm/server/commands/match_image_snapshot.d.ts +17 -0
  89. package/esm/server/commands/match_image_snapshot.d.ts.map +1 -0
  90. package/esm/server/commands/match_image_snapshot.js +88 -0
  91. package/esm/server/commands/setup_vis_suite.d.ts +13 -0
  92. package/esm/server/commands/setup_vis_suite.d.ts.map +1 -0
  93. package/esm/server/commands/setup_vis_suite.js +4 -0
  94. package/esm/server/compare_image.d.ts +8 -0
  95. package/esm/server/compare_image.d.ts.map +1 -0
  96. package/esm/server/compare_image.js +16 -0
  97. package/esm/server/file.d.ts +6 -0
  98. package/esm/server/file.d.ts.map +1 -0
  99. package/esm/server/file.js +14 -0
  100. package/esm/server/snapshot_path.d.ts +4 -0
  101. package/esm/server/snapshot_path.d.ts.map +1 -0
  102. package/esm/server/snapshot_path.js +9 -0
  103. package/esm/server/vis_context.ctx.d.ts +29 -0
  104. package/esm/server/vis_context.ctx.d.ts.map +1 -0
  105. package/esm/server/vis_context.ctx.js +11 -0
  106. package/esm/server/vis_context.d.ts +33 -0
  107. package/esm/server/vis_context.d.ts.map +1 -0
  108. package/esm/server/vis_context.js +2 -0
  109. package/esm/server/vis_context.logic.d.ts +63 -0
  110. package/esm/server/vis_context.logic.d.ts.map +1 -0
  111. package/esm/server/vis_context.logic.js +116 -0
  112. package/esm/server/vis_context.types.d.ts +20 -0
  113. package/esm/server/vis_context.types.d.ts.map +1 -0
  114. package/esm/server/vis_context.types.js +1 -0
  115. package/esm/setup/create_vis.d.ts +39 -0
  116. package/esm/setup/create_vis.d.ts.map +1 -0
  117. package/esm/setup/create_vis.js +96 -0
  118. package/esm/setup/vis.d.ts +18 -0
  119. package/esm/setup/vis.d.ts.map +1 -0
  120. package/esm/setup/vis.js +6 -0
  121. package/esm/setup.d.ts +5 -0
  122. package/esm/setup.d.ts.map +1 -0
  123. package/esm/setup.js +7 -0
  124. package/esm/shared/base64.d.ts +2 -0
  125. package/esm/shared/base64.d.ts.map +1 -0
  126. package/esm/shared/base64.js +3 -0
  127. package/esm/shared/constants.d.ts +5 -0
  128. package/esm/shared/constants.d.ts.map +1 -0
  129. package/esm/shared/constants.js +4 -0
  130. package/esm/shared/convert_threshold_unit.d.ts +6 -0
  131. package/esm/shared/convert_threshold_unit.d.ts.map +1 -0
  132. package/esm/shared/convert_threshold_unit.js +10 -0
  133. package/esm/shared/get_max_size.d.ts +11 -0
  134. package/esm/shared/get_max_size.d.ts.map +1 -0
  135. package/esm/shared/get_max_size.js +5 -0
  136. package/esm/shared/is_same_size.d.ts +8 -0
  137. package/esm/shared/is_same_size.d.ts.map +1 -0
  138. package/esm/shared/is_same_size.js +3 -0
  139. package/esm/shared/trim_common_folder.d.ts +6 -0
  140. package/esm/shared/trim_common_folder.d.ts.map +1 -0
  141. package/esm/shared/trim_common_folder.js +10 -0
  142. package/esm/shared/types.d.ts +37 -0
  143. package/esm/shared/types.d.ts.map +1 -0
  144. package/esm/shared/types.js +1 -0
  145. package/esm/testing/constants.d.ts +9 -0
  146. package/esm/testing/constants.d.ts.map +1 -0
  147. package/esm/testing/constants.js +1933 -0
  148. package/esm/testing.d.ts +2 -0
  149. package/esm/testing.d.ts.map +1 -0
  150. package/esm/testing.js +1 -0
  151. package/package.json +115 -0
  152. package/readme.md +264 -0
  153. package/src/client/ctx.ts +8 -0
  154. package/src/client/expect/expectation_result.ts +7 -0
  155. package/src/client/expect/extend.ts +12 -0
  156. package/src/client/expect/to_match_image_snapshot.logic.ts +11 -0
  157. package/src/client/expect/to_match_image_snapshot.ts +43 -0
  158. package/src/client/expect/to_match_image_snapshot.types.ts +14 -0
  159. package/src/client/page/extend.ts +8 -0
  160. package/src/client/page/has_image_snapshot.ts +33 -0
  161. package/src/client/selector.ts +118 -0
  162. package/src/client/should_take_snapshot.ts +15 -0
  163. package/src/client/snapshot_options.internal.ts +22 -0
  164. package/src/client/snapshot_options.ts +53 -0
  165. package/src/client/task_id.ts +16 -0
  166. package/src/client.ts +7 -0
  167. package/src/commands.ts +4 -0
  168. package/src/config/readme.md +4 -0
  169. package/src/config/types.ts +35 -0
  170. package/src/config/vis.ts +30 -0
  171. package/src/config.ts +3 -0
  172. package/src/index.ts +2 -0
  173. package/src/presets/auto.ts +4 -0
  174. package/src/presets/manual.ts +4 -0
  175. package/src/server/browser_provider/browser_api.ts +13 -0
  176. package/src/server/browser_provider/playwright.ts +16 -0
  177. package/src/server/browser_provider/types.ts +11 -0
  178. package/src/server/browser_provider/webdriverio.ts +13 -0
  179. package/src/server/commands/extend.ts +19 -0
  180. package/src/server/commands/has_image_snapshot.ts +18 -0
  181. package/src/server/commands/image_snapshot_next_index.ts +17 -0
  182. package/src/server/commands/is_ci.ts +7 -0
  183. package/src/server/commands/match_image_snapshot.ts +139 -0
  184. package/src/server/commands/setup_vis_suite.ts +17 -0
  185. package/src/server/compare_image.ts +34 -0
  186. package/src/server/file.ts +15 -0
  187. package/src/server/readme.md +5 -0
  188. package/src/server/snapshot_path.ts +12 -0
  189. package/src/server/vis_context.ctx.ts +12 -0
  190. package/src/server/vis_context.logic.ts +141 -0
  191. package/src/server/vis_context.ts +3 -0
  192. package/src/server/vis_context.types.ts +25 -0
  193. package/src/setup/create_vis.ts +98 -0
  194. package/src/setup/vis.ts +7 -0
  195. package/src/setup.ts +9 -0
  196. package/src/shared/base64.ts +3 -0
  197. package/src/shared/constants.ts +4 -0
  198. package/src/shared/convert_threshold_unit.ts +13 -0
  199. package/src/shared/get_max_size.ts +5 -0
  200. package/src/shared/is_same_size.ts +3 -0
  201. package/src/shared/trim_common_folder.ts +10 -0
  202. package/src/shared/types.ts +39 -0
  203. package/src/testing/constants.ts +1936 -0
  204. package/src/testing.ts +1 -0
@@ -0,0 +1,22 @@
1
+ import { NAME } from '../shared/constants.ts'
2
+ import type { MetaTask, SnapshotMeta } from './snapshot_options.ts'
3
+
4
+ export function getAutoSnapshotOptions(task: MetaTask): SnapshotMeta | undefined {
5
+ if (!task) return
6
+
7
+ const list: any[] = []
8
+ let current = task
9
+ while (current?.suite) {
10
+ list.unshift(current.suite.meta)
11
+ current = current.suite
12
+ }
13
+ list.unshift(task.file?.meta)
14
+ list.push(task.meta)
15
+ return list.reduce((acc, cur) => {
16
+ if (cur?.[NAME]) {
17
+ const { [NAME]: meta } = cur
18
+ return Object.assign({}, acc, meta)
19
+ }
20
+ return acc
21
+ }, {})
22
+ }
@@ -0,0 +1,53 @@
1
+ import { NAME } from '../shared/constants.ts'
2
+ import { ctx } from './ctx.ts'
3
+ import type { ToMatchImageSnapshotOptions } from './expect/to_match_image_snapshot.types.ts'
4
+
5
+ export type SnapshotMeta = ToMatchImageSnapshotOptions & { enable?: boolean | undefined }
6
+
7
+ type Suite = { meta: Record<string, any>; suite?: Suite | undefined }
8
+
9
+ export type MetaTask =
10
+ | {
11
+ file?: { meta: Record<string, any> } | undefined
12
+ suite?: Suite | undefined
13
+ meta: Record<string, any>
14
+ }
15
+ | undefined
16
+
17
+ /**
18
+ * Set the snapshot options for auto snapshot.
19
+ *
20
+ * ```ts
21
+ * beforeAll((suite) => setAutoSnapshotOptions(suite, ...))
22
+ * beforeEach(({ task }) => setAutoSnapshotOptions(task, ...))
23
+ *
24
+ * it('...', ({ task }) => {
25
+ * setAutoSnapshotOptions(task, ...)
26
+ * })
27
+ * ```
28
+ *
29
+ * @param task Optional. Suite or task to set the options.
30
+ * If not provided, it will set the options for the current test.
31
+ */
32
+ export function setAutoSnapshotOptions(task: MetaTask, meta: SnapshotMeta | boolean): void
33
+ export function setAutoSnapshotOptions(meta: SnapshotMeta | boolean): void
34
+ export function setAutoSnapshotOptions(
35
+ ...args: [task: MetaTask, meta: SnapshotMeta | boolean] | [meta: SnapshotMeta | boolean]
36
+ ): void {
37
+ const [task, meta] = parseArgs(args)
38
+ if (task)
39
+ task.meta[NAME] = {
40
+ ...task.meta[NAME],
41
+ ...meta,
42
+ }
43
+ }
44
+
45
+ function parseArgs(
46
+ args: [task: MetaTask, meta: SnapshotMeta | boolean] | [meta: boolean | SnapshotMeta],
47
+ ): [MetaTask | undefined, SnapshotMeta] {
48
+ return args.length === 1 ? [ctx.getCurrentTest(), parseMeta(args[0])] : [args[0], parseMeta(args[1])]
49
+ }
50
+
51
+ function parseMeta(meta: boolean | SnapshotMeta): SnapshotMeta {
52
+ return typeof meta === 'boolean' ? { enable: meta } : { enable: true, ...meta }
53
+ }
@@ -0,0 +1,16 @@
1
+ type Task = {
2
+ name: string
3
+ suite?: Task | undefined
4
+ }
5
+
6
+ export function toTaskId(task: Task) {
7
+ const l: any[] = []
8
+ let t = task
9
+ while (t?.suite) {
10
+ l.unshift(t.suite.name.replace(/[^a-z0-9]/gi, '-').toLowerCase())
11
+ t = t.suite
12
+ }
13
+
14
+ l.push(task.name.replace(/[^a-z0-9]/gi, '-').toLowerCase())
15
+ return l.join('/')
16
+ }
package/src/client.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './client/expect/expectation_result.ts'
2
+ export * from './client/expect/to_match_image_snapshot.logic.ts'
3
+ export type * from './client/expect/to_match_image_snapshot.types.ts'
4
+ export * from './client/should_take_snapshot.ts'
5
+ export * from './client/snapshot_options.ts'
6
+ export * from './client/task_id.ts'
7
+ export type * from './shared/types.ts'
@@ -0,0 +1,4 @@
1
+ export * from './server/commands/has_image_snapshot.ts'
2
+ export * from './server/commands/image_snapshot_next_index.ts'
3
+ export * from './server/commands/match_image_snapshot.ts'
4
+ export * from './server/commands/setup_vis_suite.ts'
@@ -0,0 +1,4 @@
1
+ # Config
2
+
3
+ Code in this directory is used to configure the plugin.
4
+ Vitest runs the configuration code in a NodeJS main thread.
@@ -0,0 +1,35 @@
1
+ import type { ToMatchImageSnapshotOptions } from '../client/expect/to_match_image_snapshot.types.ts'
2
+
3
+ export interface VisOptions extends ToMatchImageSnapshotOptions {
4
+ /**
5
+ * Loads the `test.setupFiles` of the specified preset.
6
+ */
7
+ preset?: 'manual' | 'auto' | 'none' | undefined
8
+ /**
9
+ * The snapshot folder relative to the root of the project.
10
+ *
11
+ * Default: `__vis__`
12
+ */
13
+ snapshotRootDir?: string | undefined
14
+ /**
15
+ * Customize the snapshot subpath.
16
+ *
17
+ * The snapshot subpath is used along with `snapshotRootDir` to determine the folder of the snapshots:
18
+ *
19
+ * - baseline: `<snapshotRootDir>/<platform>/<snapshotSubpath>/<snapshotId>.png`
20
+ * - result: `<snapshotRootDir>/__results__/<snapshotSubpath>/<snapshotId>.png`
21
+ * - diff: `<snapshotRootDir>/__diffs__/<snapshotSubpath>/<snapshotId>.png`
22
+ *
23
+ * Typically, you place your test files either in a dedicated `tests` folder or in the `src` folder along with your source code.
24
+ * By default, the snapshot subpath is the test file path with that folder removed to reduces nesting of the snapshot folders.
25
+ *
26
+ * If you place your test files in multiple folders,
27
+ * such as in both `tests` and `src` folders,
28
+ * and they might have files with the same name and create conflicting snapshots,
29
+ * you can set this to customize the snapshot subpath.
30
+ *
31
+ * @param subPath - The path of the test file to be used as the snapshot sub path.
32
+ * If you want to keep the full path, you can simply return it.
33
+ */
34
+ customizeSnapshotSubpath?: (subPath: string) => string
35
+ }
@@ -0,0 +1,30 @@
1
+ import type { Plugin } from 'vite'
2
+ import { commands } from '../server/commands/extend.ts'
3
+ import { visContext } from '../server/vis_context.ts'
4
+ import { NAME } from '../shared/constants.ts'
5
+ import type { VisOptions } from './types.ts'
6
+
7
+ /**
8
+ * Create a Vite plugin for visual testing.
9
+ *
10
+ * If options are not provided, the plugin will use the default options,
11
+ * which enables the `auto` preset.
12
+ */
13
+ export function vis(options: VisOptions = { preset: 'auto' }) {
14
+ visContext.setOptions(options)
15
+ const preset = options?.preset
16
+ return {
17
+ name: NAME,
18
+ config() {
19
+ return {
20
+ test: {
21
+ browser: {
22
+ name: undefined as unknown as string,
23
+ commands,
24
+ },
25
+ setupFiles: preset && preset !== 'none' ? [`vitest-plugin-vis/presets/${preset}`] : undefined,
26
+ },
27
+ }
28
+ },
29
+ } satisfies Plugin
30
+ }
package/src/config.ts ADDED
@@ -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'
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // `vitest-plugin-vis` provides code that can be used in test files.
2
+ export * from './client/snapshot_options.ts'
@@ -0,0 +1,4 @@
1
+ import 'vitest-browser-react'
2
+ import { vis } from '../setup/vis.ts'
3
+
4
+ vis.presets.auto()
@@ -0,0 +1,4 @@
1
+ import 'vitest-browser-react'
2
+ import { vis } from '../setup/vis.ts'
3
+
4
+ vis.presets.manual()
@@ -0,0 +1,13 @@
1
+ import type { BrowserCommandContext } from 'vitest/node'
2
+ import { playwright } from './playwright.ts'
3
+ import { webdriverio } from './webdriverio.ts'
4
+
5
+ export function browserApi(context: BrowserCommandContext) {
6
+ if (context.provider.name === 'playwright') {
7
+ return playwright(context)
8
+ }
9
+ if (context.provider.name === 'webdriverio') {
10
+ return webdriverio(context)
11
+ }
12
+ throw new Error(`Unsupported provider: ${context.provider.name}`)
13
+ }
@@ -0,0 +1,16 @@
1
+ import type { BrowserCommandContext } from 'vitest/node'
2
+ import type { BrowserApi } from './types.ts'
3
+
4
+ export function playwright(context: BrowserCommandContext): BrowserApi {
5
+ return {
6
+ async takeScreenshot(filePath, selector, options) {
7
+ // The `Locator` type from `vitest` has less props than the `Locator` in `playwright`
8
+ const subject = context.iframe.locator(selector ?? 'body')
9
+ const buffer = await subject.screenshot({
10
+ timeout: options?.timeout,
11
+ path: filePath,
12
+ })
13
+ return buffer.toString('base64')
14
+ },
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ export interface BrowserApi {
2
+ takeScreenshot(
3
+ filePath: string,
4
+ selector: string,
5
+ options:
6
+ | {
7
+ timeout?: number | undefined
8
+ }
9
+ | undefined,
10
+ ): Promise<string>
11
+ }
@@ -0,0 +1,13 @@
1
+ import type { BrowserCommandContext } from 'vitest/node'
2
+ import type { BrowserApi } from './types.ts'
3
+
4
+ export function webdriverio(context: BrowserCommandContext): BrowserApi {
5
+ const page = (context.provider as any).browser!
6
+ return {
7
+ async takeScreenshot(filePath, selector) {
8
+ const element = await page.$(`${selector ?? 'body'}`)
9
+ const buffer = await element.saveScreenshot(filePath)
10
+ return buffer.toString('base64')
11
+ },
12
+ }
13
+ }
@@ -0,0 +1,19 @@
1
+ import { type HasImageSnapshotCommand, hasImageSnapshot } from './has_image_snapshot.ts'
2
+ import { type ImageSnapshotNextIndexCommand, imageSnapshotNextIndex } from './image_snapshot_next_index.ts'
3
+ import { type MatchImageSnapshotCommand, matchImageSnapshot } from './match_image_snapshot.ts'
4
+ import { type SetupVisSuiteCommand, setupVisSuite } from './setup_vis_suite.ts'
5
+
6
+ declare module '@vitest/browser/context' {
7
+ interface BrowserCommands
8
+ extends SetupVisSuiteCommand,
9
+ ImageSnapshotNextIndexCommand,
10
+ HasImageSnapshotCommand,
11
+ MatchImageSnapshotCommand {}
12
+ }
13
+
14
+ export const commands = {
15
+ setupVisSuite,
16
+ imageSnapshotNextIndex,
17
+ hasImageSnapshot,
18
+ matchImageSnapshot,
19
+ }
@@ -0,0 +1,18 @@
1
+ import type { BrowserCommand } from 'vitest/node'
2
+ import { visContext } from '../vis_context.ts'
3
+
4
+ export interface HasImageSnapshotCommand {
5
+ hasImageSnapshot(taskId: string, snapshotId?: string | undefined): Promise<boolean>
6
+ }
7
+
8
+ export const hasImageSnapshot: BrowserCommand<[taskId: string, snapshotId?: string | undefined]> = async (
9
+ context,
10
+ taskId,
11
+ snapshotId,
12
+ ) => {
13
+ if (!context.testPath) {
14
+ throw new Error('Cannot take snapshot without testPath')
15
+ }
16
+
17
+ return visContext.hasImageSnapshot(context.testPath, taskId, snapshotId)
18
+ }
@@ -0,0 +1,17 @@
1
+ import type { BrowserCommand } from 'vitest/node'
2
+ import { visContext } from '../vis_context.ts'
3
+
4
+ export interface ImageSnapshotNextIndexCommand {
5
+ /**
6
+ * Get the index of the snapshot image to be created.
7
+ */
8
+ imageSnapshotNextIndex(taskId: string): Promise<number>
9
+ }
10
+
11
+ export const imageSnapshotNextIndex: BrowserCommand<[taskId: string]> = async (context, taskId) => {
12
+ if (!context.testPath) {
13
+ throw new Error('Cannot take snapshot without testPath')
14
+ }
15
+
16
+ return visContext.getTaskCount(context.testPath, taskId)
17
+ }
@@ -0,0 +1,7 @@
1
+ // import ci from 'is-ci'
2
+
3
+ // export interface IsCICommand {
4
+ // isCI: () => Promise<boolean>
5
+ // }
6
+
7
+ // export const isCI = async () => ci
@@ -0,0 +1,139 @@
1
+ import dedent from 'dedent'
2
+ import ci from 'is-ci'
3
+ import { mkdirp } from 'mkdirp'
4
+ import { dirname, resolve } from 'pathe'
5
+ import { PNG } from 'pngjs'
6
+ import type { BrowserCommand, BrowserCommandContext } from 'vitest/node'
7
+ import { isBase64String } from '../../shared/base64.ts'
8
+ import { getMaxSize } from '../../shared/get_max_size.ts'
9
+ import { isSameSize } from '../../shared/is_same_size.ts'
10
+ import type {
11
+ ImageSnapshotCompareOptions,
12
+ ImageSnapshotIdOptions,
13
+ ImageSnapshotTimeoutOptions,
14
+ } from '../../shared/types.ts'
15
+ import { browserApi } from '../browser_provider/browser_api.ts'
16
+ import { compareImage } from '../compare_image.ts'
17
+ import { file } from '../file.ts'
18
+ import { visContext } from '../vis_context.ts'
19
+
20
+ export interface MatchImageSnapshotCommand {
21
+ matchImageSnapshot: (
22
+ taskId: string | undefined,
23
+ subject: string,
24
+ options?: MatchImageSnapshotOptions | undefined,
25
+ ) => Promise<void>
26
+ }
27
+
28
+ export interface MatchImageSnapshotOptions
29
+ extends ImageSnapshotTimeoutOptions,
30
+ ImageSnapshotIdOptions,
31
+ ImageSnapshotCompareOptions {
32
+ /**
33
+ * The snapshot file id calculated on the client side.
34
+ */
35
+ snapshotFileId?: string | undefined
36
+ }
37
+
38
+ export const matchImageSnapshot: BrowserCommand<
39
+ [taskId: string, subject: string, options?: MatchImageSnapshotOptions | undefined]
40
+ > = async (context, taskId, subject, options) => {
41
+ if (!context.testPath) {
42
+ throw new Error('Cannot match snapshot without testPath')
43
+ }
44
+
45
+ // vitest:browser passes in `null` when not defined
46
+ if (!options) options = {}
47
+ options.timeout = options.timeout ?? (ci ? 30000 : 5000)
48
+
49
+ const info = visContext.getSnapshotInfo(context.testPath, taskId, options)
50
+ const baselineBase64 = await file.tryReadFileBase64(info.baselinePath)
51
+ if (!baselineBase64) {
52
+ await takeSnapshot(context, subject, info.baselinePath, options)
53
+ return
54
+ }
55
+
56
+ const resultBase64 = await takeSnapshot(context, subject, info.resultPath, options)
57
+ const baselineImage = PNG.sync.read(Buffer.from(baselineBase64, 'base64'))
58
+ const resultImage = PNG.sync.read(Buffer.from(resultBase64, 'base64'))
59
+ const [baselineAlignedImage, resultAlignedImage] = isSameSize(baselineImage, resultImage)
60
+ ? [baselineImage, resultImage]
61
+ : alignImageSizes(baselineImage, resultImage)
62
+
63
+ const { pass, diffAmount, diffImage } = compareImage(baselineAlignedImage, resultAlignedImage, options)
64
+ if (pass) {
65
+ if (sizeNotChanged(baselineImage, baselineAlignedImage)) {
66
+ return
67
+ }
68
+ throw new Error(
69
+ dedent`Snapshot \`${taskId}\` mismatched
70
+
71
+ The image size changed form ${baselineImage.width}x${baselineImage.height} to ${resultImage.width}x${resultImage.height}
72
+
73
+ Expected: ${resolve(context.project.runner.root, info.baselinePath)}
74
+ Actual: ${resolve(context.project.runner.root, info.resultPath)}`,
75
+ )
76
+ }
77
+
78
+ const diffBase64 = PNG.sync.write(diffImage).toString('base64')
79
+ await writeSnapshot(diffBase64, info.diffPath)
80
+
81
+ throw new Error(
82
+ dedent`Snapshot \`${taskId}\` mismatched
83
+
84
+ ${
85
+ options?.failureThreshold
86
+ ? options?.failureThresholdType === 'percent'
87
+ ? `Expected image to match within ${options.failureThreshold}% but was differ by ${diffAmount}%.`
88
+ : `Expected image to match within ${options.failureThreshold} pixels but was differ by ${diffAmount} pixels.`
89
+ : `Expected image to match but was differ by ${options?.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`}.`
90
+ }
91
+
92
+ Options: ${JSON.stringify(options)}
93
+
94
+ Expected: ${resolve(context.project.runner.root, info.baselinePath)}
95
+ Actual: ${resolve(context.project.runner.root, info.resultPath)}
96
+ Difference: ${resolve(context.project.runner.root, info.diffPath)}`,
97
+ )
98
+ }
99
+
100
+ async function takeSnapshot(
101
+ context: BrowserCommandContext,
102
+ subject: string,
103
+ filePath: string,
104
+ options: ImageSnapshotTimeoutOptions | undefined,
105
+ ) {
106
+ if (isBase64String(subject)) {
107
+ return writeSnapshot(subject, filePath)
108
+ }
109
+
110
+ await mkdirp(dirname(filePath))
111
+ const browser = browserApi(context)
112
+ return browser.takeScreenshot(filePath, subject, {
113
+ timeout: options?.timeout,
114
+ })
115
+ }
116
+
117
+ async function writeSnapshot(subject: string, filePath: string) {
118
+ await mkdirp(dirname(filePath))
119
+ await file.writeFileBase64(filePath, subject)
120
+ return subject
121
+ }
122
+
123
+ function alignImageSizes(baseline: PNG, result: PNG) {
124
+ const size = getMaxSize(baseline, result)
125
+
126
+ const baselineAligned = new PNG(size)
127
+ const resultAligned = new PNG(size)
128
+
129
+ baselineAligned.data.fill(0)
130
+ resultAligned.data.fill(0)
131
+
132
+ PNG.bitblt(baseline, baselineAligned, 0, 0, baseline.width, baseline.height)
133
+ PNG.bitblt(result, resultAligned, 0, 0, result.width, result.height)
134
+
135
+ return [baselineAligned, resultAligned] as const
136
+ }
137
+ function sizeNotChanged(baselineImage: PNG, baselineAlignedImage: PNG) {
138
+ return baselineImage === baselineAlignedImage
139
+ }
@@ -0,0 +1,17 @@
1
+ import type { BrowserCommand } from 'vitest/node'
2
+ import { visContext } from '../vis_context.ts'
3
+
4
+ export interface SetupVisSuiteCommand {
5
+ /**
6
+ * Sets up the visual testing suite by cleaning up previous snapshot and diff output directories.
7
+ * When this command is run for the first time,
8
+ * it will remove the results and diffs directory of all suites.
9
+ *
10
+ * In subsequent runs, it will only remove the results and diffs directory of the current suite.
11
+ */
12
+ setupVisSuite: () => Promise<void>
13
+ }
14
+
15
+ export const setupVisSuite: BrowserCommand<[]> = async (context) => {
16
+ await visContext.setupSuite(context)
17
+ }
@@ -0,0 +1,34 @@
1
+ import pixelmatch from 'pixelmatch'
2
+ import { PNG } from 'pngjs'
3
+ import { convertThresholdUnit } from '../shared/convert_threshold_unit.ts'
4
+ import type { ImageSnapshotCompareOptions } from '../shared/types.ts'
5
+
6
+ export function compareImage(
7
+ baselineImage: PNG,
8
+ resultImage: PNG,
9
+ { failureThreshold = 0, failureThresholdType = 'pixel', diffOptions }: ImageSnapshotCompareOptions = {},
10
+ ) {
11
+ const diffImage = new PNG({
12
+ width: baselineImage.width,
13
+ height: baselineImage.height,
14
+ })
15
+
16
+ const pixelDiff = pixelmatch(
17
+ resultImage.data,
18
+ baselineImage.data,
19
+ diffImage.data,
20
+ baselineImage.width,
21
+ baselineImage.height,
22
+ diffOptions,
23
+ )
24
+ const diffAmount = convertThresholdUnit(
25
+ { failureThresholdType, width: baselineImage.width, height: baselineImage.height },
26
+ pixelDiff,
27
+ )
28
+
29
+ return {
30
+ pass: diffAmount <= failureThreshold,
31
+ diffAmount,
32
+ diffImage,
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ import { readFile, stat, writeFile } from 'node:fs/promises'
2
+
3
+ export const file = {
4
+ async tryReadFileBase64(filePath: string): Promise<string | undefined> {
5
+ return readFile(filePath, { encoding: 'base64' }).catch(() => undefined)
6
+ },
7
+ writeFileBase64(filePath: string, data: string) {
8
+ return writeFile(filePath, data, { encoding: 'base64' })
9
+ },
10
+ existFile(filePath: string) {
11
+ return stat(filePath)
12
+ .then((s) => s.isFile())
13
+ .catch(() => false)
14
+ },
15
+ }
@@ -0,0 +1,5 @@
1
+ # Server
2
+
3
+ This is the server part of the plugin.
4
+ Vitest Browser runs this code in a NodeJS worker.
5
+ It is a separate process from the config/main process.
@@ -0,0 +1,12 @@
1
+ import type { VisOptions } from '../config/types.ts'
2
+ import { SNAPSHOT_ROOT_DIR } from '../shared/constants.ts'
3
+ import { trimCommonFolder } from '../shared/trim_common_folder.ts'
4
+
5
+ export function resolveSnapshotRootDir(options: VisOptions | undefined) {
6
+ return options?.snapshotRootDir ?? SNAPSHOT_ROOT_DIR
7
+ }
8
+
9
+ export function getSnapshotSubpath(suiteName: string, options: VisOptions) {
10
+ const customizeSnapshotSubpath = options.customizeSnapshotSubpath ?? trimCommonFolder
11
+ return customizeSnapshotSubpath(suiteName)
12
+ }
@@ -0,0 +1,12 @@
1
+ /* v8 ignore start */
2
+ import ci from 'is-ci'
3
+ import { rimraf } from 'rimraf'
4
+
5
+ export const ctx = {
6
+ rimraf,
7
+ getSnapshotPlatform() {
8
+ /* v8 ignore next */
9
+ return ci ? process.platform : 'local'
10
+ },
11
+ }
12
+ /* v8 ignore end */