@histoire/plugin-percy 0.17.8 → 0.17.13

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/index.d.ts CHANGED
@@ -1,29 +1,65 @@
1
1
  import type { Plugin } from 'histoire';
2
+ import type { Page, WaitForOptions } from 'puppeteer';
3
+ /**
4
+ * Percy Snapshot Options
5
+ * Not official type, just for reference
6
+ * @see https://www.browserstack.com/docs/percy/take-percy-snapshots/snapshots-via-scripts
7
+ */
8
+ export interface PercySnapshotOptions {
9
+ widths?: number[];
10
+ minHeight?: number;
11
+ percyCSS?: string;
12
+ enableJavaScript?: boolean;
13
+ discovery?: Partial<{
14
+ allowedHostnames: string[];
15
+ disallowedHostnames: string[];
16
+ requestHeaders: Record<string, string>;
17
+ authorization: Partial<{
18
+ username: string;
19
+ password: string;
20
+ }>;
21
+ disableCache: boolean;
22
+ userAgent: string;
23
+ }>;
24
+ }
25
+ export type PagePayload = {
26
+ file: string;
27
+ story: {
28
+ title: string;
29
+ };
30
+ variant: {
31
+ id: string;
32
+ title: string;
33
+ };
34
+ };
35
+ type ContructorOption<T extends object | number> = T | ((payload: PagePayload) => T);
2
36
  export interface PercyPluginOptions {
3
37
  /**
4
38
  * Ignored stories.
5
39
  */
6
- ignored?: (payload: {
7
- file: string;
8
- story: {
9
- title: string;
10
- };
11
- variant: {
12
- id: string;
13
- title: string;
14
- };
15
- }) => boolean;
40
+ ignored?: (payload: PagePayload) => boolean;
16
41
  /**
17
42
  * Percy options.
18
43
  */
19
- percyOptions?: any;
44
+ percyOptions?: ContructorOption<PercySnapshotOptions>;
20
45
  /**
21
46
  * Delay puppeteer page screenshot after page load
22
47
  */
23
- pptrWait?: number;
48
+ pptrWait?: ContructorOption<number>;
24
49
  /**
25
50
  * Navigation Parameter
26
51
  */
27
- pptrOptions?: any;
52
+ pptrOptions?: ContructorOption<WaitForOptions & {
53
+ referer?: string;
54
+ }>;
55
+ /**
56
+ * Before taking a snapshot, you can modify the page
57
+ * It happens after the page is loaded and wait (if pptrWait is passed) and before the snapshot is taken
58
+ *
59
+ * @param page Puppeteer page
60
+ * @returns Promise<void | boolean> - If it returns false, the snapshot will be skipped
61
+ */
62
+ beforeSnapshot?: (page: Page, payload: PagePayload) => Promise<void | boolean>;
28
63
  }
29
64
  export declare function HstPercy(options?: PercyPluginOptions): Plugin;
65
+ export {};
package/dist/index.js CHANGED
@@ -10,12 +10,15 @@ const defaultOptions = {
10
10
  pptrWait: 0,
11
11
  pptrOptions: {},
12
12
  };
13
+ function resolveOptions(option, payload) {
14
+ return typeof option === 'function' ? option(payload) : option;
15
+ }
13
16
  export function HstPercy(options = {}) {
14
17
  const finalOptions = defu(options, defaultOptions);
15
18
  return {
16
19
  name: '@histoire/plugin-percy',
17
20
  onBuild: async (api) => {
18
- if (!await isPercyEnabled()) {
21
+ if (!(await isPercyEnabled())) {
19
22
  return;
20
23
  }
21
24
  const puppeteer = await import('puppeteer');
@@ -26,7 +29,7 @@ export function HstPercy(options = {}) {
26
29
  const CLIENT_INFO = `${sdkPkg.name}/${sdkPkg.version}`;
27
30
  const ENV_INFO = `${puppeteerPkg.name}/${puppeteerPkg.version}`;
28
31
  api.onPreviewStory(async ({ file, story, variant, url }) => {
29
- if (finalOptions.ignored?.({
32
+ const payload = {
30
33
  file,
31
34
  story: {
32
35
  title: story.title,
@@ -35,20 +38,30 @@ export function HstPercy(options = {}) {
35
38
  id: variant.id,
36
39
  title: variant.title,
37
40
  },
38
- })) {
41
+ };
42
+ if (finalOptions.ignored?.(payload)) {
39
43
  return;
40
44
  }
45
+ const pptrOptions = resolveOptions(finalOptions.pptrOptions, payload);
46
+ const pptrWait = resolveOptions(finalOptions.pptrWait, payload);
47
+ const percyOptions = resolveOptions(finalOptions.percyOptions, payload);
41
48
  const page = await browser.newPage();
42
- await page.goto(url, finalOptions.pptrOptions);
43
- await new Promise(resolve => setTimeout(resolve, finalOptions.pptrWait));
49
+ await page.goto(url, pptrOptions);
50
+ await new Promise((resolve) => setTimeout(resolve, pptrWait));
51
+ if (finalOptions.beforeSnapshot) {
52
+ const result = await finalOptions.beforeSnapshot(page, payload);
53
+ if (result === false) {
54
+ return;
55
+ }
56
+ }
44
57
  const name = `${story.title} > ${variant.title}`;
45
58
  await page.evaluate(await fetchPercyDOM());
46
59
  const domSnapshot = await page.evaluate((opts) => {
47
60
  // @ts-expect-error window global var
48
61
  return window.PercyDOM.serialize(opts);
49
- }, finalOptions.percyOptions);
62
+ }, percyOptions);
50
63
  await postSnapshot({
51
- ...finalOptions.percyOptions,
64
+ ...percyOptions,
52
65
  environmentInfo: ENV_INFO,
53
66
  clientInfo: CLIENT_INFO,
54
67
  url: page.url(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@histoire/plugin-percy",
3
- "version": "0.17.8",
3
+ "version": "0.17.13",
4
4
  "description": "Histoire plugin to take screenshots with Percy for visual regression testing",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -32,10 +32,10 @@
32
32
  "devDependencies": {
33
33
  "@types/node": "^18.11.9",
34
34
  "typescript": "^4.9.5",
35
- "histoire": "0.17.8"
35
+ "histoire": "0.17.9"
36
36
  },
37
37
  "peerDependencies": {
38
- "histoire": "^0.17.8"
38
+ "histoire": "^0.17.9"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "rimraf dist && tsc -d",
package/src/index.ts CHANGED
@@ -4,29 +4,79 @@ import path from 'pathe'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { createRequire } from 'node:module'
6
6
  import { isPercyEnabled, fetchPercyDOM, postSnapshot } from '@percy/sdk-utils'
7
+ import type { JSONObject, Page, WaitForOptions } from 'puppeteer'
7
8
 
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
10
  const require = createRequire(import.meta.url)
10
11
 
12
+ /**
13
+ * Percy Snapshot Options
14
+ * Not official type, just for reference
15
+ * @see https://www.browserstack.com/docs/percy/take-percy-snapshots/snapshots-via-scripts
16
+ */
17
+ export interface PercySnapshotOptions {
18
+ widths?: number[]
19
+ minHeight?: number
20
+ percyCSS?: string
21
+ enableJavaScript?: boolean
22
+ discovery?: Partial<{
23
+ allowedHostnames: string[]
24
+ disallowedHostnames: string[]
25
+ requestHeaders: Record<string, string>
26
+ authorization: Partial<{
27
+ username: string
28
+ password: string
29
+ }>
30
+ disableCache: boolean
31
+ userAgent: string
32
+ }>
33
+ }
34
+
35
+ export type PagePayload = {
36
+ file: string
37
+ story: { title: string }
38
+ variant: { id: string, title: string }
39
+ };
40
+
41
+ type ContructorOption<T extends object | number> =
42
+ | T
43
+ | ((payload: PagePayload) => T);
44
+
11
45
  export interface PercyPluginOptions {
12
46
  /**
13
47
  * Ignored stories.
14
48
  */
15
- ignored?: (payload: { file: string, story: { title: string }, variant: { id: string, title: string } }) => boolean
49
+ ignored?: (payload: PagePayload) => boolean
16
50
  /**
17
51
  * Percy options.
18
52
  */
19
- percyOptions?: any
53
+ percyOptions?: ContructorOption<PercySnapshotOptions>
20
54
 
21
55
  /**
22
56
  * Delay puppeteer page screenshot after page load
23
57
  */
24
- pptrWait?: number
58
+ pptrWait?: ContructorOption<number>
25
59
 
26
60
  /**
27
61
  * Navigation Parameter
28
62
  */
29
- pptrOptions?: any
63
+ pptrOptions?: ContructorOption<
64
+ WaitForOptions & {
65
+ referer?: string
66
+ }
67
+ >
68
+
69
+ /**
70
+ * Before taking a snapshot, you can modify the page
71
+ * It happens after the page is loaded and wait (if pptrWait is passed) and before the snapshot is taken
72
+ *
73
+ * @param page Puppeteer page
74
+ * @returns Promise<void | boolean> - If it returns false, the snapshot will be skipped
75
+ */
76
+ beforeSnapshot?: (
77
+ page: Page,
78
+ payload: PagePayload
79
+ ) => Promise<void | boolean>
30
80
  }
31
81
 
32
82
  const defaultOptions: PercyPluginOptions = {
@@ -35,13 +85,20 @@ const defaultOptions: PercyPluginOptions = {
35
85
  pptrOptions: {},
36
86
  }
37
87
 
88
+ function resolveOptions<T extends object | number> (
89
+ option: ContructorOption<T>,
90
+ payload: PagePayload,
91
+ ): T {
92
+ return typeof option === 'function' ? option(payload) : option
93
+ }
94
+
38
95
  export function HstPercy (options: PercyPluginOptions = {}): Plugin {
39
96
  const finalOptions: PercyPluginOptions = defu(options, defaultOptions)
40
97
  return {
41
98
  name: '@histoire/plugin-percy',
42
99
 
43
- onBuild: async api => {
44
- if (!await isPercyEnabled()) {
100
+ onBuild: async (api) => {
101
+ if (!(await isPercyEnabled())) {
45
102
  return
46
103
  }
47
104
 
@@ -55,7 +112,7 @@ export function HstPercy (options: PercyPluginOptions = {}): Plugin {
55
112
  const ENV_INFO = `${puppeteerPkg.name}/${puppeteerPkg.version}`
56
113
 
57
114
  api.onPreviewStory(async ({ file, story, variant, url }) => {
58
- if (finalOptions.ignored?.({
115
+ const payload = {
59
116
  file,
60
117
  story: {
61
118
  title: story.title,
@@ -64,23 +121,36 @@ export function HstPercy (options: PercyPluginOptions = {}): Plugin {
64
121
  id: variant.id,
65
122
  title: variant.title,
66
123
  },
67
- })) {
124
+ }
125
+
126
+ if (finalOptions.ignored?.(payload)) {
68
127
  return
69
128
  }
70
129
 
130
+ const pptrOptions = resolveOptions(finalOptions.pptrOptions, payload)
131
+ const pptrWait = resolveOptions(finalOptions.pptrWait, payload)
132
+ const percyOptions = resolveOptions(finalOptions.percyOptions, payload)
133
+
71
134
  const page = await browser.newPage()
72
- await page.goto(url, finalOptions.pptrOptions)
135
+ await page.goto(url, pptrOptions)
136
+
137
+ await new Promise((resolve) => setTimeout(resolve, pptrWait))
73
138
 
74
- await new Promise(resolve => setTimeout(resolve, finalOptions.pptrWait))
139
+ if (finalOptions.beforeSnapshot) {
140
+ const result = await finalOptions.beforeSnapshot(page, payload)
141
+ if (result === false) {
142
+ return
143
+ }
144
+ }
75
145
 
76
146
  const name = `${story.title} > ${variant.title}`
77
147
  await page.evaluate(await fetchPercyDOM())
78
148
  const domSnapshot = await page.evaluate((opts) => {
79
149
  // @ts-expect-error window global var
80
150
  return window.PercyDOM.serialize(opts)
81
- }, finalOptions.percyOptions)
151
+ }, percyOptions as JSONObject)
82
152
  await postSnapshot({
83
- ...finalOptions.percyOptions,
153
+ ...percyOptions,
84
154
  environmentInfo: ENV_INFO,
85
155
  clientInfo: CLIENT_INFO,
86
156
  url: page.url(),