Skip to content

Commit 2036ba4

Browse files
omacrangerLogan Graham
and
Logan Graham
authored
[Storybook] Add ability to take snapshots during Storybook play interactions (#168)
Co-authored-by: Logan Graham <[email protected]>
1 parent 7d3f733 commit 2036ba4

File tree

9 files changed

+193
-24
lines changed

9 files changed

+193
-24
lines changed
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@saucelabs/visual-storybook": minor
3+
---
4+
5+
add play interaction snapshot testing
6+
re-add storybook 6 support

visual-js/visual-storybook/.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.work
22
build/
33
coverage/
4-
.parent/
4+
.parent/
5+
play.*

visual-js/visual-storybook/package.json

+21-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"types": "build/index.d.ts",
88
"license": "MIT",
99
"files": [
10-
"build"
10+
"build",
11+
"play.*"
1112
],
1213
"type": "module",
1314
"engines": {
@@ -33,6 +34,10 @@
3334
"require": "./build/config/global-teardown.cjs",
3435
"import": "./build/config/global-teardown.js"
3536
},
37+
"./play": {
38+
"require": "./play.cjs",
39+
"import": "./play.js"
40+
},
3641
"./package.json": "./package.json"
3742
},
3843
"scripts": {
@@ -44,34 +49,39 @@
4449
"dependencies": {
4550
"@saucelabs/visual": "^0.10.0",
4651
"@saucelabs/visual-playwright": "^0.2.0",
52+
"@storybook/core-events": "^6.4.0 || ^7.0.0 || ^8.0.0",
53+
"@storybook/instrumenter": "^6.4.0 || ^7.0.0 || ^8.0.0",
4754
"@storybook/test-runner": ">=0.13.0",
4855
"exponential-backoff": "^3.1.1",
4956
"jest-playwright-preset": "^2.0.0 || ^3.0.0"
5057
},
5158
"peerDependencies": {
52-
"@storybook/core": "^7.0.0 || ^8.0.0",
53-
"storybook": "^7.0.0 || ^8.0.0"
59+
"storybook": "^6.4.0 || ^7.0.0 || ^8.0.0"
5460
},
5561
"tsup": {
56-
"entry": [
57-
"./src/index.ts",
58-
"./src/config/global-setup.ts",
59-
"./src/config/global-teardown.ts"
60-
],
62+
"entry": {
63+
"build/index": "./src/index.ts",
64+
"build/config/global-setup": "./src/config/global-setup.ts",
65+
"build/config/global-teardown": "./src/config/global-teardown.ts",
66+
"play": "./src/play.ts"
67+
},
6168
"dts": true,
62-
"outDir": "./build",
69+
"outDir": "./",
6370
"format": [
6471
"cjs",
6572
"esm"
6673
],
6774
"external": [
6875
"@saucelabs/visual-storybook"
6976
],
70-
"noExternal": []
77+
"noExternal": [],
78+
"splitting": false
7179
},
7280
"devDependencies": {
7381
"@jest/globals": "^28.0.0 || ^29.0.0",
74-
"@storybook/types": "^8.0.2",
82+
"@storybook/core-events": "^8.4.5",
83+
"@storybook/instrumenter": "^8.4.5",
84+
"@storybook/types": "^8.4.5",
7585
"@tsconfig/node18": "^2.0.0",
7686
"@types/node": "^18.13.0",
7787
"@types/node-fetch": "^2.6.4",

visual-js/visual-storybook/src/api.ts

+43-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { TestContext } from '@storybook/test-runner';
22
import { getStoryContext } from '@storybook/test-runner';
33
import type { Page } from 'playwright-core';
4-
import { internals } from '@saucelabs/visual-playwright';
5-
import { SauceVisualParams, StoryContext, StoryVariation } from './types';
4+
import {
5+
internals,
6+
SauceVisualParams as PlaywrightParams,
7+
} from '@saucelabs/visual-playwright';
8+
import type { SauceVisualParams, StoryContext, StoryVariation } from './types';
9+
import type { Channel } from '@storybook/core/channels';
610
import events from '@storybook/core/core-events';
711

8-
import type EventEmitter from 'node:events';
9-
1012
const { VisualPlaywright } = internals;
1113

1214
const clientVersion = 'PKG_VERSION';
@@ -97,7 +99,7 @@ export const postVisit = async (page: Page, context: TestContext) => {
9799
await page.evaluate(
98100
({ variation, events, storyId }) => {
99101
// @ts-expect-error Global managed by Storybook.
100-
const channel: EventEmitter = globalThis.__STORYBOOK_ADDONS_CHANNEL__;
102+
const channel: Channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__;
101103
if (!channel) {
102104
throw new Error(
103105
'The test runner could not access the Storybook channel. Are you sure the Storybook is running correctly in that URL?',
@@ -143,3 +145,39 @@ export const postVisit = async (page: Page, context: TestContext) => {
143145
* `postVisit` exported from this package instead.
144146
*/
145147
export const postRender = postVisit;
148+
149+
/**
150+
* Playwright throws an exception if attempting to expose the same binding twice and does not
151+
* expose a way for us to see if something has already been bound. Since we're only given access
152+
* to the Page object during postVisit (not during setup) we can't ensure that it's only added once.
153+
* This is just a simple check to see if the current instance has already been bound and skip
154+
* double binding if so.
155+
*/
156+
let hasExposed = false;
157+
158+
/**
159+
* Used in Storybook's test runner config file (test-runner.js/ts) for the `preVisit` hook. Preps
160+
* the binding for taking visual snapshots during and after render / execution.
161+
*/
162+
export const preVisit = async (page: Page, context: TestContext) => {
163+
if (hasExposed) {
164+
return;
165+
}
166+
167+
await page.exposeBinding(
168+
'takeVisualSnapshot',
169+
async (source, ...args: [string, PlaywrightParams | undefined]) => {
170+
const [name, params] = args;
171+
172+
const storyContext = await getStoryContext(source.page, context);
173+
174+
await takeScreenshot(
175+
augmentStoryName(storyContext, {
176+
name,
177+
}),
178+
params,
179+
);
180+
},
181+
);
182+
hasExposed = true;
183+
};
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export { postRender, postVisit } from './api';
1+
export { postRender, postVisit, preVisit } from './api';
22
export { getVisualTestConfig } from './config';
33
export type { SauceVisualParams, ArgsTypes, StoryVariation } from './types';
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { SauceVisualParams as PlaywrightParams } from '@saucelabs/visual-playwright';
2+
import { instrument } from '@storybook/instrumenter';
3+
4+
const _takeVisualSnapshot = async (
5+
name: string,
6+
params?: PlaywrightParams,
7+
): Promise<void> => {
8+
/**
9+
* @see https://github.com/storybookjs/test-runner?tab=readme-ov-file#storybooktestrunner-user-agent
10+
*/
11+
const isTestRunner = window.navigator.userAgent.match(/StorybookTestRunner/);
12+
13+
if (!isTestRunner) {
14+
console.info(
15+
'Skipping Sauce Visual snapshot -- not in test runner context.',
16+
);
17+
return;
18+
}
19+
if (!window.takeVisualSnapshot) {
20+
throw new Error(
21+
'`takeVisualSnapshot` is not available. Did you setup your `preVisit` hook for Sauce Labs in your Storybook test-runner.js/ts configuration file?',
22+
);
23+
}
24+
await window.takeVisualSnapshot(name, params);
25+
};
26+
27+
export const {
28+
/**
29+
* Takes a screenshot with Sauce Visual. Designed to be used only within the Storybook Test Runner
30+
* execution. Is noop when not in the test runner.
31+
* @param name
32+
* @param params
33+
*/
34+
takeVisualSnapshot,
35+
} = instrument(
36+
{
37+
takeVisualSnapshot: _takeVisualSnapshot,
38+
},
39+
{
40+
intercept: true,
41+
},
42+
);

visual-js/visual-storybook/src/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { SauceRegion } from '@saucelabs/visual';
2-
import { SauceVisualParams as PlaywrightParams } from '@saucelabs/visual-playwright';
1+
import type { SauceRegion } from '@saucelabs/visual';
2+
import type { SauceVisualParams as PlaywrightParams } from '@saucelabs/visual-playwright';
33

44
export interface VisualOpts extends PlaywrightParams {
55
user: string | undefined;
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
/* eslint-disable no-var */
2-
import { getApi } from '@saucelabs/visual';
2+
import type { getApi } from '@saucelabs/visual';
3+
import type { SauceVisualParams as PlaywrightParams } from '@saucelabs/visual-playwright';
34

45
declare global {
56
var visualApi: ReturnType<typeof getApi>;
67
var buildId: string;
8+
interface Window {
9+
takeVisualSnapshot?: (
10+
name: string,
11+
opts?: PlaywrightParams,
12+
) => Promise<void>;
13+
}
714
}

visual-js/yarn.lock

+68-3
Original file line numberDiff line numberDiff line change
@@ -3066,8 +3066,10 @@ __metadata:
30663066
"@jest/globals": ^28.0.0 || ^29.0.0
30673067
"@saucelabs/visual": ^0.10.0
30683068
"@saucelabs/visual-playwright": ^0.2.0
3069+
"@storybook/core-events": ^8.4.5
3070+
"@storybook/instrumenter": ^8.4.5
30693071
"@storybook/test-runner": ">=0.13.0"
3070-
"@storybook/types": ^8.0.2
3072+
"@storybook/types": ^8.4.5
30713073
"@tsconfig/node18": ^2.0.0
30723074
"@types/node": ^18.13.0
30733075
"@types/node-fetch": ^2.6.4
@@ -3090,8 +3092,7 @@ __metadata:
30903092
tsup: ^7.2.0
30913093
typescript: ^5.0.4
30923094
peerDependencies:
3093-
"@storybook/core": ^7.0.0 || ^8.0.0
3094-
storybook: ^7.0.0 || ^8.0.0
3095+
storybook: ^6.4.0 || ^7.0.0 || ^8.0.0
30953096
languageName: unknown
30963097
linkType: soft
30973098

@@ -3231,6 +3232,15 @@ __metadata:
32313232
languageName: node
32323233
linkType: hard
32333234

3235+
"@storybook/core-events@npm:^8.4.5":
3236+
version: 8.4.5
3237+
resolution: "@storybook/core-events@npm:8.4.5"
3238+
peerDependencies:
3239+
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
3240+
checksum: d7322c6d8723f98b7a0caf897048f53e4bacfbeb3ebb53a215b42a92222f1f8af4749bede2358af06973bf8e64df874299f342ab30747685c486d1d5d8b8cd28
3241+
languageName: node
3242+
linkType: hard
3243+
32343244
"@storybook/core@npm:8.3.5":
32353245
version: 8.3.5
32363246
resolution: "@storybook/core@npm:8.3.5"
@@ -3270,6 +3280,25 @@ __metadata:
32703280
languageName: node
32713281
linkType: hard
32723282

3283+
"@storybook/global@npm:^5.0.0":
3284+
version: 5.0.0
3285+
resolution: "@storybook/global@npm:5.0.0"
3286+
checksum: ede0ad35ec411fe31c61150dbd118fef344d1d0e72bf5d3502368e35cf68126f6b7ae4a0ab5e2ffe2f0baa3b4286f03ad069ba3e098e1725449ef08b7e154ba8
3287+
languageName: node
3288+
linkType: hard
3289+
3290+
"@storybook/instrumenter@npm:^8.4.5":
3291+
version: 8.4.5
3292+
resolution: "@storybook/instrumenter@npm:8.4.5"
3293+
dependencies:
3294+
"@storybook/global": ^5.0.0
3295+
"@vitest/utils": ^2.1.1
3296+
peerDependencies:
3297+
storybook: ^8.4.5
3298+
checksum: 14093e36b14871e74331074b7261f9103ddfe1233dffa7f722a8e36c955cb5812d7e3a202b8a7673dbdceb10853d0989cef1ff8e928b058635b2032188905842
3299+
languageName: node
3300+
linkType: hard
3301+
32733302
"@storybook/preview-api@npm:^8.0.0":
32743303
version: 8.3.5
32753304
resolution: "@storybook/preview-api@npm:8.3.5"
@@ -3320,6 +3349,15 @@ __metadata:
33203349
languageName: node
33213350
linkType: hard
33223351

3352+
"@storybook/types@npm:^8.4.5":
3353+
version: 8.4.5
3354+
resolution: "@storybook/types@npm:8.4.5"
3355+
peerDependencies:
3356+
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
3357+
checksum: 9c6d8aef05a475ed42faa63618ee6a958803115fb124281d2724576935630c769166ea5ea3dcabe69a28658d7f190ea3acffbeddd9c0aeb0894688df2ed4fe40
3358+
languageName: node
3359+
linkType: hard
3360+
33233361
"@swc/core-darwin-arm64@npm:1.7.35":
33243362
version: 1.7.35
33253363
resolution: "@swc/core-darwin-arm64@npm:1.7.35"
@@ -4105,6 +4143,15 @@ __metadata:
41054143
languageName: node
41064144
linkType: hard
41074145

4146+
"@vitest/pretty-format@npm:2.1.6":
4147+
version: 2.1.6
4148+
resolution: "@vitest/pretty-format@npm:2.1.6"
4149+
dependencies:
4150+
tinyrainbow: ^1.2.0
4151+
checksum: 4cab9152ac97fa190db85bbe7e1ae8f1b5d2312fa3ccf7e813119933b2aaf4c763c6156a6d91cb186d3ed4be81f5bb70da2c731fde2d12457fe0871087d2be74
4152+
languageName: node
4153+
linkType: hard
4154+
41084155
"@vitest/snapshot@npm:^2.0.3":
41094156
version: 2.1.3
41104157
resolution: "@vitest/snapshot@npm:2.1.3"
@@ -4116,6 +4163,17 @@ __metadata:
41164163
languageName: node
41174164
linkType: hard
41184165

4166+
"@vitest/utils@npm:^2.1.1":
4167+
version: 2.1.6
4168+
resolution: "@vitest/utils@npm:2.1.6"
4169+
dependencies:
4170+
"@vitest/pretty-format": 2.1.6
4171+
loupe: ^3.1.2
4172+
tinyrainbow: ^1.2.0
4173+
checksum: 8b9c994eccb724d76128e875e8438d519bfae0126e7431e8682e7f07d9faeff929db1afa2742b188883e42508d4cdcb2326f9ae27c1b53b5f746d283a9e75462
4174+
languageName: node
4175+
linkType: hard
4176+
41194177
"@wdio/config@npm:8.10.4":
41204178
version: 8.10.4
41214179
resolution: "@wdio/config@npm:8.10.4"
@@ -10703,6 +10761,13 @@ __metadata:
1070310761
languageName: node
1070410762
linkType: hard
1070510763

10764+
"loupe@npm:^3.1.2":
10765+
version: 3.1.2
10766+
resolution: "loupe@npm:3.1.2"
10767+
checksum: 4a75bbe8877a1ced3603e08b1095cd6f4c987c50fe63719fdc3009029560f91e07a915e7f6eff1322bb62bfb2a2beeef06b13ccb3c12f81bda9f3674434dcab9
10768+
languageName: node
10769+
linkType: hard
10770+
1070610771
"lower-case-first@npm:^2.0.2":
1070710772
version: 2.0.2
1070810773
resolution: "lower-case-first@npm:2.0.2"

0 commit comments

Comments
 (0)