vitest-plugin-vis 1.0.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/esm/client/align_images.d.ts +2 -0
  2. package/esm/client/align_images.d.ts.map +1 -0
  3. package/esm/client/align_images.js +10 -0
  4. package/esm/client/expect/to_match_image_snapshot.d.ts.map +1 -1
  5. package/esm/client/expect/to_match_image_snapshot.js +2 -15
  6. package/esm/client/image_data.d.ts +3 -0
  7. package/esm/client/image_data.d.ts.map +1 -0
  8. package/esm/client/image_data.js +33 -0
  9. package/esm/client/image_resizer.d.ts +9 -0
  10. package/esm/client/image_resizer.d.ts.map +1 -0
  11. package/esm/client/image_resizer.js +49 -0
  12. package/esm/client/match_image_snapshot.d.ts +8 -0
  13. package/esm/client/match_image_snapshot.d.ts.map +1 -0
  14. package/esm/client/match_image_snapshot.js +65 -0
  15. package/esm/client/match_image_snapshot.logic.d.ts +3 -0
  16. package/esm/client/match_image_snapshot.logic.d.ts.map +1 -0
  17. package/esm/client/match_image_snapshot.logic.js +11 -0
  18. package/esm/client/task_id.d.ts +3 -0
  19. package/esm/client/task_id.d.ts.map +1 -1
  20. package/esm/client/task_id.js +3 -0
  21. package/esm/client/vitest_browser_context_proxy.d.ts +32 -0
  22. package/esm/client/vitest_browser_context_proxy.d.ts.map +1 -0
  23. package/esm/client/vitest_browser_context_proxy.js +11 -0
  24. package/esm/client.d.ts +1 -1
  25. package/esm/client.d.ts.map +1 -1
  26. package/esm/client.js +1 -1
  27. package/esm/config/vis.d.ts +2 -0
  28. package/esm/config/vis.d.ts.map +1 -1
  29. package/esm/presets/auto.d.ts +1 -1
  30. package/esm/presets/auto.d.ts.map +1 -1
  31. package/esm/presets/auto.js +0 -1
  32. package/esm/presets/manual.d.ts +1 -1
  33. package/esm/presets/manual.d.ts.map +1 -1
  34. package/esm/presets/manual.js +0 -1
  35. package/esm/server/browser_provider/playwright.js +1 -1
  36. package/esm/server/browser_provider/webdriverio.js +1 -1
  37. package/esm/server/commands/_assertions.d.ts +5 -0
  38. package/esm/server/commands/_assertions.d.ts.map +1 -0
  39. package/esm/server/commands/_assertions.js +11 -0
  40. package/esm/server/commands/extend.d.ts +5 -1
  41. package/esm/server/commands/extend.d.ts.map +1 -1
  42. package/esm/server/commands/extend.js +4 -0
  43. package/esm/server/commands/has_image_snapshot.d.ts.map +1 -1
  44. package/esm/server/commands/has_image_snapshot.js +2 -3
  45. package/esm/server/commands/image_snapshot_next_index.d.ts.map +1 -1
  46. package/esm/server/commands/image_snapshot_next_index.js +2 -3
  47. package/esm/server/commands/match_image_snapshot.d.ts.map +1 -1
  48. package/esm/server/commands/match_image_snapshot.js +9 -31
  49. package/esm/server/commands/prepare_image_snapshot_comparison.d.ts +20 -0
  50. package/esm/server/commands/prepare_image_snapshot_comparison.d.ts.map +1 -0
  51. package/esm/server/commands/prepare_image_snapshot_comparison.js +31 -0
  52. package/esm/server/commands/setup_vis_suite.d.ts.map +1 -1
  53. package/esm/server/commands/setup_vis_suite.js +2 -0
  54. package/esm/server/commands/write_image_snapshot.d.ts +12 -0
  55. package/esm/server/commands/write_image_snapshot.d.ts.map +1 -0
  56. package/esm/server/commands/write_image_snapshot.js +9 -0
  57. package/esm/server/snapshot.d.ts +7 -0
  58. package/esm/server/snapshot.d.ts.map +1 -0
  59. package/esm/server/snapshot.js +27 -0
  60. package/esm/server/vis_context.d.ts.map +1 -1
  61. package/esm/server/vis_context.logic.d.ts +1 -1
  62. package/esm/server/vis_context.logic.d.ts.map +1 -1
  63. package/esm/shared/compare_image.d.ts +6 -0
  64. package/esm/shared/compare_image.d.ts.map +1 -0
  65. package/esm/shared/compare_image.js +10 -0
  66. package/esm/shared/convert_threshold_unit.js +1 -1
  67. package/esm/testing/stubBrowserCommandContext.d.ts +3 -0
  68. package/esm/testing/stubBrowserCommandContext.d.ts.map +1 -0
  69. package/esm/testing/stubBrowserCommandContext.js +2 -0
  70. package/esm/testing.d.ts +1 -0
  71. package/esm/testing.d.ts.map +1 -1
  72. package/esm/testing.js +1 -0
  73. package/package.json +3 -2
  74. package/readme.md +22 -4
  75. package/src/client/align_images.ts +11 -0
  76. package/src/client/expect/to_match_image_snapshot.ts +2 -17
  77. package/src/client/image_data.ts +36 -0
  78. package/src/client/image_resizer.ts +59 -0
  79. package/src/client/match_image_snapshot.logic.ts +13 -0
  80. package/src/client/match_image_snapshot.ts +87 -0
  81. package/src/client/task_id.ts +3 -0
  82. package/src/client/vitest_browser_context_proxy.ts +44 -0
  83. package/src/client.ts +1 -1
  84. package/src/presets/auto.ts +0 -1
  85. package/src/presets/manual.ts +0 -1
  86. package/src/server/browser_provider/playwright.ts +1 -1
  87. package/src/server/browser_provider/webdriverio.ts +1 -1
  88. package/src/server/commands/_assertions.ts +18 -0
  89. package/src/server/commands/extend.ts +9 -0
  90. package/src/server/commands/has_image_snapshot.ts +2 -3
  91. package/src/server/commands/image_snapshot_next_index.ts +2 -3
  92. package/src/server/commands/match_image_snapshot.ts +17 -46
  93. package/src/server/commands/prepare_image_snapshot_comparison.ts +54 -0
  94. package/src/server/commands/setup_vis_suite.ts +3 -0
  95. package/src/server/commands/write_image_snapshot.ts +26 -0
  96. package/src/server/snapshot.ts +42 -0
  97. package/src/server/vis_context.logic.ts +2 -2
  98. package/src/shared/compare_image.ts +20 -0
  99. package/src/shared/convert_threshold_unit.ts +1 -1
  100. package/src/testing/stubBrowserCommandContext.ts +4 -0
  101. package/src/testing.ts +1 -0
  102. package/esm/client/expect/to_match_image_snapshot.logic.d.ts +0 -2
  103. package/esm/client/expect/to_match_image_snapshot.logic.d.ts.map +0 -1
  104. package/esm/client/expect/to_match_image_snapshot.logic.js +0 -11
  105. package/esm/server/commands/is_ci.d.ts +0 -2
  106. package/esm/server/commands/is_ci.d.ts.map +0 -1
  107. package/esm/server/commands/is_ci.js +0 -6
  108. package/esm/server/compare_image.d.ts +0 -8
  109. package/esm/server/compare_image.d.ts.map +0 -1
  110. package/esm/server/compare_image.js +0 -16
  111. package/src/client/expect/to_match_image_snapshot.logic.ts +0 -11
  112. package/src/server/commands/is_ci.ts +0 -7
  113. package/src/server/compare_image.ts +0 -34
@@ -0,0 +1,20 @@
1
+ import type { BrowserCommand } from 'vitest/node';
2
+ import type { MatchImageSnapshotOptions } from './match_image_snapshot.ts';
3
+ type ImageSnapshotComparisonInfo = {
4
+ projectRoot: string;
5
+ baselinePath: string;
6
+ resultPath: string;
7
+ diffPath: string;
8
+ baseline: string;
9
+ result: string;
10
+ };
11
+ export interface PrepareImageSnapshotComparisonCommand {
12
+ prepareImageSnapshotComparison: (taskId: string | undefined, subject: string, options?: MatchImageSnapshotOptions | undefined) => Promise<ImageSnapshotComparisonInfo | undefined>;
13
+ }
14
+ export declare const prepareImageSnapshotComparison: BrowserCommand<[
15
+ taskId: string,
16
+ snapshotId: string,
17
+ options?: MatchImageSnapshotOptions | undefined
18
+ ]>;
19
+ export {};
20
+ //# sourceMappingURL=prepare_image_snapshot_comparison.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepare_image_snapshot_comparison.d.ts","sourceRoot":"","sources":["../../../src/server/commands/prepare_image_snapshot_comparison.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAMjD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAA;AAE1E,KAAK,2BAA2B,GAAG;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,WAAW,qCAAqC;IACrD,8BAA8B,EAAE,CAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,yBAAyB,GAAG,SAAS,KAC3C,OAAO,CAAC,2BAA2B,GAAG,SAAS,CAAC,CAAA;CACrD;AAED,eAAO,MAAM,8BAA8B,EAAE,cAAc,CAC1D;IAAC,MAAM,EAAE,MAAM;IAAE,UAAU,EAAE,MAAM;IAAE,OAAO,CAAC,EAAE,yBAAyB,GAAG,SAAS;CAAC,CA0BrF,CAAA"}
@@ -0,0 +1,31 @@
1
+ import ci from 'is-ci';
2
+ import { isBase64String } from "../../shared/base64.js";
3
+ import { file } from "../file.js";
4
+ import { takeSnapshot, takeSnapshotByBrowser, writeSnapshot } from "../snapshot.js";
5
+ import { visContext } from "../vis_context.js";
6
+ import { assertTestPathDefined } from "./_assertions.js";
7
+ export const prepareImageSnapshotComparison = async (context, taskId, subject, options) => {
8
+ assertTestPathDefined(context, 'prepareImageSnapshotComparison');
9
+ // vitest:browser passes in `null` when not defined
10
+ if (!options)
11
+ options = {};
12
+ options.timeout = options.timeout ?? (ci ? 30000 : 5000);
13
+ const info = visContext.getSnapshotInfo(context.testPath, taskId, options);
14
+ const baselineBuffer = await file.tryReadFile(info.baselinePath);
15
+ if (!baselineBuffer) {
16
+ if (isBase64String(subject)) {
17
+ await writeSnapshot(info.baselinePath, subject);
18
+ }
19
+ else {
20
+ await takeSnapshotByBrowser(context, info.baselinePath, subject, options);
21
+ }
22
+ return;
23
+ }
24
+ const resultBuffer = await takeSnapshot(context, info.resultPath, subject, options);
25
+ return {
26
+ ...info,
27
+ projectRoot: context.project.runner.root,
28
+ baseline: baselineBuffer.toString('base64'),
29
+ result: resultBuffer.toString('base64'),
30
+ };
31
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"setup_vis_suite.d.ts","sourceRoot":"","sources":["../../../src/server/commands/setup_vis_suite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAGjD,MAAM,WAAW,oBAAoB;IACpC;;;;;;OAMG;IACH,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED,eAAO,MAAM,aAAa,EAAE,cAAc,CAAC,EAAE,CAE5C,CAAA"}
1
+ {"version":3,"file":"setup_vis_suite.d.ts","sourceRoot":"","sources":["../../../src/server/commands/setup_vis_suite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,MAAM,WAAW,oBAAoB;IACpC;;;;;;OAMG;IACH,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED,eAAO,MAAM,aAAa,EAAE,cAAc,CAAC,EAAE,CAI5C,CAAA"}
@@ -1,4 +1,6 @@
1
1
  import { visContext } from "../vis_context.js";
2
+ import { assertTestPathDefined } from "./_assertions.js";
2
3
  export const setupVisSuite = async (context) => {
4
+ assertTestPathDefined(context, 'setupVisSuite');
3
5
  await visContext.setupSuite(context);
4
6
  };
@@ -0,0 +1,12 @@
1
+ import type { BrowserCommand } from 'vitest/node';
2
+ export interface WriteImageSnapshotCommand {
3
+ /**
4
+ * Writes the image snapshot to the file system.
5
+ * @param relativeFilePath The file path to write the image snapshot to.
6
+ * It is relative to the project root.
7
+ * @param imageBase64 The base64 encoded image.
8
+ */
9
+ writeImageSnapshot(relativeFilePath: string, imageBase64: string): Promise<void>;
10
+ }
11
+ export declare const writeImageSnapshot: BrowserCommand<[relativeFilePath: string, imageBase64: string]>;
12
+ //# sourceMappingURL=write_image_snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write_image_snapshot.d.ts","sourceRoot":"","sources":["../../../src/server/commands/write_image_snapshot.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,MAAM,WAAW,yBAAyB;IACzC;;;;;OAKG;IACH,kBAAkB,CAAC,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAChF;AAED,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,CAAC,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,CAS9F,CAAA"}
@@ -0,0 +1,9 @@
1
+ import { mkdirp } from 'mkdirp';
2
+ import { dirname, isAbsolute } from 'pathe';
3
+ import { file } from "../file.js";
4
+ import { assertIsRelativePath } from "./_assertions.js";
5
+ export const writeImageSnapshot = async (_, relativeFilePath, imageBase64) => {
6
+ assertIsRelativePath(relativeFilePath, 'relativeFilePath');
7
+ await mkdirp(dirname(relativeFilePath));
8
+ await file.writeFile(relativeFilePath, imageBase64, { encoding: 'base64' });
9
+ };
@@ -0,0 +1,7 @@
1
+ import type { BrowserCommandContext } from 'vitest/node';
2
+ import type { ImageSnapshotTimeoutOptions } from '../client.ts';
3
+ export declare function takeSnapshot(context: BrowserCommandContext, filePath: string, subject: string, options: ImageSnapshotTimeoutOptions | undefined): Promise<Buffer<ArrayBufferLike>>;
4
+ export declare function takeSnapshotByBrowser(context: BrowserCommandContext, filePath: string, subject: string, options: ImageSnapshotTimeoutOptions | undefined): Promise<Buffer<ArrayBufferLike>>;
5
+ export declare function writeSnapshot(filePath: string, subject: string): Promise<void>;
6
+ export declare function writeSnapshotBuffer(filePath: string, subject: Buffer): Promise<void>;
7
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/server/snapshot.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAA;AAK/D,wBAAsB,YAAY,CACjC,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,2BAA2B,GAAG,SAAS,oCAOhD;AAED,wBAAsB,qBAAqB,CAC1C,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,2BAA2B,GAAG,SAAS,oCAOhD;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAGpE;AACD,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAG1E"}
@@ -0,0 +1,27 @@
1
+ import { mkdirp } from 'mkdirp';
2
+ import { dirname } from 'pathe';
3
+ import { isBase64String } from "../shared/base64.js";
4
+ import { browserApi } from "./browser_provider/browser_api.js";
5
+ import { file } from './file.js';
6
+ export async function takeSnapshot(context, filePath, subject, options) {
7
+ if (isBase64String(subject)) {
8
+ await writeSnapshot(filePath, subject);
9
+ return Buffer.from(subject, 'base64');
10
+ }
11
+ return takeSnapshotByBrowser(context, filePath, subject, options);
12
+ }
13
+ export async function takeSnapshotByBrowser(context, filePath, subject, options) {
14
+ await mkdirp(dirname(filePath));
15
+ const browser = browserApi(context);
16
+ return browser.takeScreenshot(filePath, subject ?? 'body', {
17
+ timeout: options?.timeout,
18
+ });
19
+ }
20
+ export async function writeSnapshot(filePath, subject) {
21
+ await mkdirp(dirname(filePath));
22
+ await file.writeFile(filePath, subject, { encoding: 'base64' });
23
+ }
24
+ export async function writeSnapshotBuffer(filePath, subject) {
25
+ await mkdirp(dirname(filePath));
26
+ await file.writeFile(filePath, subject);
27
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"vis_context.d.ts","sourceRoot":"","sources":["../../src/server/vis_context.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,UAAU;;;;;;;sBAC4iD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;CADxhD,CAAA"}
1
+ {"version":3,"file":"vis_context.d.ts","sourceRoot":"","sources":["../../src/server/vis_context.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,UAAU;;;;;;;sBAC+hD,CAAC;;;;;;;;;;;;;;;;;;;;;;;;CAD3gD,CAAA"}
@@ -11,7 +11,7 @@ export type PartialBrowserCommandContext = {
11
11
  hookTimeout: number;
12
12
  };
13
13
  };
14
- testPath: string | undefined;
14
+ testPath: string;
15
15
  };
16
16
  export declare function createVisContext(): {
17
17
  setOptions(options?: VisOptions): void;
@@ -1 +1 @@
1
- {"version":3,"file":"vis_context.logic.d.ts","sourceRoot":"","sources":["../../src/server/vis_context.logic.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAKpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAEtD,MAAM,MAAM,4BAA4B,GAAG;IAC1C,OAAO,EAAE;QACR,MAAM,EAAE;YACP,IAAI,EAAE,MAAM,CAAA;YACZ,eAAe,EAAE;gBAChB,cAAc,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAA;aACtC,CAAA;YACD,WAAW,EAAE,MAAM,CAAA;YACnB,WAAW,EAAE,MAAM,CAAA;SACnB,CAAA;KACD,CAAA;IACD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B,CAAA;AAED,wBAAgB,gBAAgB;yBAMV,UAAU;;;;IAY9B;;;;OAIG;wBACuB,4BAA4B;8BAY5B,MAAM,QAAQ,MAAM,YAAY;QAAE,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE;;;;;2BAiB1E,MAAM,UAAU,MAAM;+BAGlB,MAAM,UAAU,MAAM,kBAAkB,MAAM,GAAG,SAAS;8BAI3D;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,kBAAkB,MAAM,GAAG,SAAS;2BAMlF,MAAM,UAAU,MAAM;;;;;;;;;;EAe9C;AAuBD,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,UAEhF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU;;;;;;;;EAWjF"}
1
+ {"version":3,"file":"vis_context.logic.d.ts","sourceRoot":"","sources":["../../src/server/vis_context.logic.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAKpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAEtD,MAAM,MAAM,4BAA4B,GAAG;IAC1C,OAAO,EAAE;QACR,MAAM,EAAE;YACP,IAAI,EAAE,MAAM,CAAA;YACZ,eAAe,EAAE;gBAChB,cAAc,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAA;aACtC,CAAA;YACD,WAAW,EAAE,MAAM,CAAA;YACnB,WAAW,EAAE,MAAM,CAAA;SACnB,CAAA;KACD,CAAA;IACD,QAAQ,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,wBAAgB,gBAAgB;yBAMV,UAAU;;;;IAY9B;;;;OAIG;wBACuB,4BAA4B;8BAY5B,MAAM,QAAQ,MAAM,YAAY;QAAE,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE;;;;;2BAiB1E,MAAM,UAAU,MAAM;+BAGlB,MAAM,UAAU,MAAM,kBAAkB,MAAM,GAAG,SAAS;8BAI3D;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,kBAAkB,MAAM,GAAG,SAAS;2BAMlF,MAAM,UAAU,MAAM;;;;;;;;;;EAe9C;AAuBD,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,UAEhF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU;;;;;;;;EAWjF"}
@@ -0,0 +1,6 @@
1
+ import type { ImageSnapshotCompareOptions } from './types.ts';
2
+ export declare function compareImage(img1: Uint8ClampedArray | Buffer, img2: Uint8ClampedArray | Buffer, output: Uint8ClampedArray | Buffer, width: number, height: number, { failureThreshold, failureThresholdType, diffOptions }?: ImageSnapshotCompareOptions): {
3
+ pass: boolean;
4
+ diffAmount: number;
5
+ };
6
+ //# sourceMappingURL=compare_image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare_image.d.ts","sourceRoot":"","sources":["../../src/shared/compare_image.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAA;AAE7D,wBAAgB,YAAY,CAC3B,IAAI,EAAE,iBAAiB,GAAG,MAAM,EAChC,IAAI,EAAE,iBAAiB,GAAG,MAAM,EAChC,MAAM,EAAE,iBAAiB,GAAG,MAAM,EAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,EAAE,gBAAoB,EAAE,oBAA8B,EAAE,WAAW,EAAE,GAAE,2BAAgC;;;EASvG"}
@@ -0,0 +1,10 @@
1
+ import pixelmatch from 'pixelmatch';
2
+ import { convertThresholdUnit } from "./convert_threshold_unit.js";
3
+ export function compareImage(img1, img2, output, width, height, { failureThreshold = 0, failureThresholdType = 'pixel', diffOptions } = {}) {
4
+ const pixelDiff = pixelmatch(img1, img2, output, width, height, diffOptions);
5
+ const diffAmount = convertThresholdUnit({ failureThresholdType, width, height }, pixelDiff);
6
+ return {
7
+ pass: diffAmount <= failureThreshold,
8
+ diffAmount,
9
+ };
10
+ }
@@ -5,6 +5,6 @@ export function convertThresholdUnit({ failureThresholdType, width, height }, pi
5
5
  case 'percent':
6
6
  return (pixelDiff / (width * height)) * 100;
7
7
  default:
8
- throw new Error(`Invalid failureThresholdType: ${failureThresholdType}`);
8
+ throw new Error(`Unsupported failureThresholdType: ${failureThresholdType}`);
9
9
  }
10
10
  }
@@ -0,0 +1,3 @@
1
+ import type { BrowserCommandContext } from 'vitest/node';
2
+ export declare const stubBrowserCommandContext: (stub?: import("type-plus").RecursivePartial<BrowserCommandContext> | undefined) => BrowserCommandContext;
3
+ //# sourceMappingURL=stubBrowserCommandContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stubBrowserCommandContext.d.ts","sourceRoot":"","sources":["../../src/testing/stubBrowserCommandContext.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAExD,eAAO,MAAM,yBAAyB,2GAAwC,CAAA"}
@@ -0,0 +1,2 @@
1
+ import { stub } from 'type-plus';
2
+ export const stubBrowserCommandContext = stub.build({});
package/esm/testing.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './testing/constants.ts';
2
+ export * from './testing/stubBrowserCommandContext.ts';
2
3
  //# sourceMappingURL=testing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAA"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAA;AACtC,cAAc,wCAAwC,CAAA"}
package/esm/testing.js CHANGED
@@ -1 +1,2 @@
1
1
  export * from "./testing/constants.js";
2
+ export * from "./testing/stubBrowserCommandContext.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-plugin-vis",
3
- "version": "1.0.5",
3
+ "version": "1.2.0",
4
4
  "description": "Vitest visual testing plugin",
5
5
  "keywords": [
6
6
  "image-snapshot",
@@ -64,7 +64,7 @@
64
64
  "dedent": "^1.5.1",
65
65
  "is-ci": "^4.0.0",
66
66
  "mkdirp": "^3.0.1",
67
- "pathe": "^1.1.2",
67
+ "pathe": "^2.0.0",
68
68
  "pixelmatch": "^6.0.0",
69
69
  "pngjs": "^7.0.0",
70
70
  "rimraf": "^6.0.1",
@@ -80,6 +80,7 @@
80
80
  "@vitest/coverage-v8": "^2.1.8",
81
81
  "@vitest/expect": "^2.1.8",
82
82
  "cross-env": "^7.0.3",
83
+ "never-fail": "^1.1.4",
83
84
  "npm-run-all2": "^7.0.2",
84
85
  "playwright": "^1.49.0",
85
86
  "react": "^19.0.0",
package/readme.md CHANGED
@@ -53,14 +53,14 @@ You can customize the configuration:
53
53
  ```ts
54
54
  // vitest.config.ts
55
55
  import { defineConfig } from 'vitest/config'
56
- import { vis } from 'vitest-plugin-vis/config'
56
+ import { vis, trimCommonFolder } from 'vitest-plugin-vis/config'
57
57
 
58
58
  export default defineConfig({
59
59
  plugins: [
60
60
  vis({
61
61
  preset: 'auto',
62
62
  snapshotRootDir: '__vis__',
63
- customizeSnapshotSubpath: (subpath) => subpath,
63
+ customizeSnapshotSubpath: (subpath) => trimCommonFolder(subpath),
64
64
  customizeSnapshotId: (id, index) => `${id}-${index}`,
65
65
  diffOptions: undefined, // pixelmatch options
66
66
  failureThresholdType: 'pixel',
@@ -103,7 +103,7 @@ vis.presets.auto()
103
103
  vis.presets.manual()
104
104
 
105
105
 
106
- // or capture image snapshot for all stories with `snapshot` tag,
106
+ // or capture image snapshot for all rendering tests
107
107
  // for multiple themes (light and dark in this example)
108
108
  vis.presets.theme({
109
109
  light() { document.body.classList.remove('dark') },
@@ -115,7 +115,8 @@ vis.presets.theme({
115
115
 
116
116
  ### Auto Snapshot
117
117
 
118
- By default, the plugin will use the `auto` preset, which will take a snapshot at the end of each test.
118
+ By default, the plugin will use the `auto` preset,
119
+ which will take a snapshot at the end of each test.
119
120
 
120
121
  You can control how the auto snapshot is taken using the `setAutoSnapshotOptions` function:
121
122
 
@@ -153,6 +154,23 @@ describe('nested scope', () => {
153
154
  })
154
155
  ```
155
156
 
157
+ The options are similar to `expect(...).toMatchImageSnapshot(options)`:
158
+
159
+ ```ts
160
+ // enable/disable auto snapshot
161
+ setAutoSnapshotOptions(true /* or false */)
162
+
163
+ // detailed options
164
+ setAutoSnapshotOptions({
165
+ enable: true,
166
+ customizeSnapshotId: (id, index) => `${id}-custom-${index}`,
167
+ diffOptions: { threshold: 0.01 } // pixelmatch options,
168
+ failureThreshold: 0.01,
169
+ failureThresholdType: 'percent',
170
+ timeout: 15000
171
+ })
172
+ ```
173
+
156
174
  ### Manual Snapshot
157
175
 
158
176
  You can also set the preset to `manual` and compare snapshots manually:
@@ -0,0 +1,11 @@
1
+ import { getMaxSize } from '../shared/get_max_size.ts'
2
+ import { isSameSize } from '../shared/is_same_size.ts'
3
+ import { createImageResizer } from './image_resizer.ts'
4
+
5
+ export function alignImagesToSameSize(image1: ImageData, image2: ImageData): [image1: ImageData, image2: ImageData] {
6
+ if (isSameSize(image1, image2)) return [image1, image2] as const
7
+
8
+ const size = getMaxSize(image1, image2)
9
+ const resize = createImageResizer(size)
10
+ return [resize(image1), resize(image2)] as const
11
+ }
@@ -1,9 +1,7 @@
1
- import { commands } from '@vitest/browser/context'
2
1
  import type { AsyncExpectationResult } from '@vitest/expect'
3
2
  import { ctx } from '../ctx.ts'
4
- import { toTaskId } from '../task_id.ts'
3
+ import { matchImageSnapshot } from '../match_image_snapshot.ts'
5
4
  import { success } from './expectation_result.ts'
6
- import { parseImageSnapshotSubject } from './to_match_image_snapshot.logic.ts'
7
5
  import type { ToMatchImageSnapshotOptions } from './to_match_image_snapshot.types.ts'
8
6
 
9
7
  export function toMatchImageSnapshot(
@@ -26,18 +24,5 @@ export function toMatchImageSnapshot(
26
24
  )
27
25
  }
28
26
 
29
- const s = parseImageSnapshotSubject(subject)
30
- const taskId = toTaskId(test)
31
- return toMatchImageSnapshotAsync(taskId, s, options).then(() => success)
32
- }
33
-
34
- async function toMatchImageSnapshotAsync(taskId: string, subject: string, options?: ToMatchImageSnapshotOptions) {
35
- if (options?.customizeSnapshotId) {
36
- const index = await commands.imageSnapshotNextIndex(taskId)
37
- const { customizeSnapshotId, ...rest } = options
38
- const snapshotFileId = customizeSnapshotId(taskId, index)
39
- return commands.matchImageSnapshot(taskId, subject, { ...rest, snapshotFileId })
40
- }
41
-
42
- return commands.matchImageSnapshot(taskId, subject, options)
27
+ return matchImageSnapshot(test, subject, options).then(() => success)
43
28
  }
@@ -0,0 +1,36 @@
1
+ export function toImageData(base64: string) {
2
+ return new Promise<ImageData>((resolve, reject) => {
3
+ const img = new Image()
4
+ img.src = `data:image/png;base64,${base64}`
5
+
6
+ img.onload = () => {
7
+ const canvas = document.createElement('canvas')
8
+ canvas.width = img.width
9
+ canvas.height = img.height
10
+ const ctx = canvas.getContext('2d')!
11
+ ctx.drawImage(img, 0, 0)
12
+ resolve(ctx.getImageData(0, 0, canvas.width, canvas.height))
13
+ }
14
+
15
+ img.onerror = () => {
16
+ reject(new Error('Failed to load image'))
17
+ }
18
+ })
19
+ }
20
+
21
+ export async function toDataURL(imageData: ImageData) {
22
+ const canvas = document.createElement('canvas')
23
+ canvas.width = imageData.width
24
+ canvas.height = imageData.height
25
+ const ctx = canvas.getContext('2d')!
26
+ ctx.putImageData(imageData, 0, 0)
27
+ return new Promise<string>((resolve) => {
28
+ canvas.toBlob((blob) => {
29
+ const reader = new FileReader()
30
+ reader.onload = () => {
31
+ resolve(reader.result as string)
32
+ }
33
+ reader.readAsDataURL(blob!)
34
+ })
35
+ })
36
+ }
@@ -0,0 +1,59 @@
1
+ import { isSameSize } from '../shared/is_same_size.ts'
2
+
3
+ export const createImageResizer =
4
+ ({ width, height }: { width: number; height: number }) =>
5
+ (image: ImageData) => {
6
+ if (isSameSize(image, { width, height })) return image
7
+
8
+ const inArea = (x: number, y: number) => y <= image.height && x <= image.width
9
+
10
+ const data = new Uint8ClampedArray(width * height * 4)
11
+ const result = new ImageData(data, width, height, { colorSpace: image.colorSpace })
12
+
13
+ for (let y = 0; y < height; y++) {
14
+ for (let x = 0; x < width; x++) {
15
+ const idx = (width * y + x) << 2
16
+ if (inArea(x, y)) {
17
+ const old = (image.width * y + x) << 2
18
+ result.data[idx] = image.data[old]!
19
+ result.data[idx + 1] = image.data[old + 1]!
20
+ result.data[idx + 2] = image.data[old + 2]!
21
+ result.data[idx + 3] = image.data[old + 3]!
22
+ } else {
23
+ result.data[idx] = 0
24
+ result.data[idx + 1] = 0
25
+ result.data[idx + 2] = 0
26
+ result.data[idx + 3] = 64
27
+ }
28
+ }
29
+ }
30
+
31
+ return result
32
+ }
33
+
34
+ // slower
35
+ export const createImageResizer2 =
36
+ ({ width, height }: { width: number; height: number }) =>
37
+ (image: ImageData) => {
38
+ if (isSameSize(image, { width, height })) return image
39
+
40
+ const inArea = (x: number, y: number) => y <= image.height && x <= image.width
41
+
42
+ const data = new Uint8ClampedArray(width * height * 4)
43
+ const result = new ImageData(data, width, height, { colorSpace: image.colorSpace })
44
+
45
+ const copyBytes = image.width * 4
46
+ for (let y = 0; y < height; y++) {
47
+ result.data.set(image.data.slice(y * image.width, copyBytes), y * width * 4)
48
+ for (let x = image.width; x < width; x++) {
49
+ const idx = (width * y + x) << 2
50
+ if (!inArea(x, y)) {
51
+ result.data[idx] = 0
52
+ result.data[idx + 1] = 0
53
+ result.data[idx + 2] = 0
54
+ result.data[idx + 3] = 64
55
+ }
56
+ }
57
+ }
58
+ return result
59
+ }
@@ -0,0 +1,13 @@
1
+ import type { ToMatchImageSnapshotOptions } from './expect/to_match_image_snapshot.types.ts'
2
+
3
+ export function prettifyOptions(options: ToMatchImageSnapshotOptions | undefined) {
4
+ if (!options) return 'none'
5
+
6
+ return [
7
+ `failureThreshold: ${options.failureThreshold ?? 0} ${options.failureThresholdType ?? 'pixel'}`,
8
+ options.diffOptions ? `diffOptions: ${JSON.stringify(options.diffOptions)}` : '',
9
+ options.timeout ? `timeout: ${options.timeout} ms` : '',
10
+ ]
11
+ .filter(Boolean)
12
+ .join(', ')
13
+ }
@@ -0,0 +1,87 @@
1
+ import { commands } from '@vitest/browser/context'
2
+ import dedent from 'dedent'
3
+ import { resolve } from 'pathe'
4
+ import type { Task } from 'vitest'
5
+ import { isBase64String } from '../shared/base64.ts'
6
+ import { compareImage } from '../shared/compare_image.ts'
7
+ import { alignImagesToSameSize } from './align_images.ts'
8
+ import type { ToMatchImageSnapshotOptions } from './expect/to_match_image_snapshot.types.ts'
9
+ import { toDataURL, toImageData } from './image_data.ts'
10
+ import { prettifyOptions } from './match_image_snapshot.logic.ts'
11
+ import { convertElementToCssSelector } from './selector.ts'
12
+ import { toTaskId } from './task_id.ts'
13
+ import { server } from './vitest_browser_context_proxy.ts'
14
+
15
+ export async function matchImageSnapshot(test: Task, subject: any, options?: ToMatchImageSnapshotOptions) {
16
+ const taskId = toTaskId(test)
17
+ const info = await commands.prepareImageSnapshotComparison(
18
+ taskId,
19
+ parseImageSnapshotSubject(subject),
20
+ options?.customizeSnapshotId ? await parseImageSnapshotOptions(taskId, options) : options,
21
+ )
22
+
23
+ if (!info) return
24
+
25
+ const baselineImage = await toImageData(info.baseline)
26
+ const resultImage = await toImageData(info.result)
27
+ const [baselineAlignedImage, resultAlignedImage] = alignImagesToSameSize(baselineImage, resultImage)
28
+ const { width, height } = baselineAlignedImage
29
+ const diffImage = new ImageData(width, height)
30
+ const { pass, diffAmount } = compareImage(
31
+ baselineAlignedImage.data,
32
+ resultAlignedImage.data,
33
+ diffImage.data,
34
+ width,
35
+ height,
36
+ options,
37
+ )
38
+ if (pass) return
39
+ if (server.config.snapshotOptions.updateSnapshot === 'all') {
40
+ await writeSnapshot(info.baselinePath, resultImage)
41
+ return
42
+ }
43
+
44
+ await writeSnapshot(info.diffPath, diffImage)
45
+
46
+ throw new Error(
47
+ dedent`Snapshot \`${taskId}\` mismatched
48
+
49
+ ${
50
+ options?.failureThreshold
51
+ ? options?.failureThresholdType === 'percent'
52
+ ? `Expected image to match within ${options.failureThreshold}% but was differ by ${diffAmount}%.`
53
+ : `Expected image to match within ${options.failureThreshold} pixels but was differ by ${diffAmount} pixels.`
54
+ : `Expected image to match but was differ by ${options?.failureThresholdType === 'percent' ? `${diffAmount}%` : `${diffAmount} pixels`}.`
55
+ }
56
+
57
+ Options: ${prettifyOptions(options)}
58
+
59
+ Expected: ${resolve(info.projectRoot, info.baselinePath)}
60
+ Actual: ${resolve(info.projectRoot, info.resultPath)}
61
+ Difference: ${resolve(info.projectRoot, info.diffPath)}`,
62
+ )
63
+ }
64
+
65
+ /**
66
+ * @deprecated internalized. Use `matchImageSnapshot` directly instead.
67
+ */
68
+ export function parseImageSnapshotSubject(subject: any) {
69
+ if (subject instanceof Element) return convertElementToCssSelector(subject)
70
+ if (subject?.['selector']) return subject['selector']
71
+ if (isBase64String(subject)) return subject
72
+ throw new Error(
73
+ `'toMatchImageSnapshot()' expects the subject to be an element, locator, or image encoded in base64 string, but got: ${subject}`,
74
+ )
75
+ }
76
+
77
+ async function writeSnapshot(path: string, image: ImageData) {
78
+ const content = (await toDataURL(image)).split(',')[1]!
79
+ return commands.writeImageSnapshot(path, content)
80
+ }
81
+
82
+ async function parseImageSnapshotOptions(taskId: string, options: ToMatchImageSnapshotOptions) {
83
+ const index = await commands.imageSnapshotNextIndex(taskId)
84
+ const { customizeSnapshotId, ...rest } = options
85
+ const snapshotFileId = customizeSnapshotId!(taskId, index)
86
+ return { ...rest, snapshotFileId }
87
+ }
@@ -3,6 +3,9 @@ type Task = {
3
3
  suite?: Task | undefined
4
4
  }
5
5
 
6
+ /**
7
+ * @deprecated not needed anymore. Use `matchImageSnapshot` directly instead.
8
+ */
6
9
  export function toTaskId(task: Task) {
7
10
  const l: any[] = []
8
11
  let t = task
@@ -0,0 +1,44 @@
1
+ import type { BrowserCommands, Platform } from '@vitest/browser/context'
2
+ import type { SerializedConfig } from 'vitest'
3
+
4
+ let ctx: Awaited<typeof import('@vitest/browser/context')>
5
+
6
+ if ((globalThis as any).__vitest_browser__) {
7
+ import('@vitest/browser/context').then((m) => {
8
+ ctx = m
9
+ })
10
+ }
11
+
12
+ export const server = new Proxy<{
13
+ /**
14
+ * Platform the Vitest server is running on.
15
+ * The same as calling `process.platform` on the server.
16
+ */
17
+ platform: Platform
18
+ /**
19
+ * Runtime version of the Vitest server.
20
+ * The same as calling `process.version` on the server.
21
+ */
22
+ version: string
23
+ /**
24
+ * Name of the browser provider.
25
+ */
26
+ provider: string
27
+ /**
28
+ * Name of the current browser.
29
+ */
30
+ browser: string
31
+ /**
32
+ * Available commands for the browser.
33
+ * @see {@link https://vitest.dev/guide/browser/commands}
34
+ */
35
+ commands: BrowserCommands
36
+ /**
37
+ * Serialized test config.
38
+ */
39
+ config: SerializedConfig
40
+ }>({} as any, {
41
+ get(target, prop) {
42
+ return (target as any)[prop] ?? (ctx?.server as any)[prop]
43
+ },
44
+ })
package/src/client.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from './client/expect/expectation_result.ts'
2
- export * from './client/expect/to_match_image_snapshot.logic.ts'
3
2
  export type * from './client/expect/to_match_image_snapshot.types.ts'
3
+ export * from './client/match_image_snapshot.ts'
4
4
  export * from './client/should_take_snapshot.ts'
5
5
  export * from './client/snapshot_options.ts'
6
6
  export * from './client/task_id.ts'
@@ -1,4 +1,3 @@
1
- import 'vitest-browser-react'
2
1
  import { vis } from '../setup/vis.ts'
3
2
 
4
3
  vis.presets.auto()
@@ -1,4 +1,3 @@
1
- import 'vitest-browser-react'
2
1
  import { vis } from '../setup/vis.ts'
3
2
 
4
3
  vis.presets.manual()
@@ -5,7 +5,7 @@ export function playwright(context: BrowserCommandContext): BrowserApi {
5
5
  return {
6
6
  async takeScreenshot(filePath, selector, options) {
7
7
  // The `Locator` type from `vitest` has less props than the `Locator` in `playwright`
8
- const subject = context.iframe.locator(selector ?? 'body')
8
+ const subject = context.iframe.locator(selector)
9
9
  return subject.screenshot({
10
10
  timeout: options?.timeout,
11
11
  path: filePath,