Skip to content

Commit

Permalink
[Storybook] Add ability to take snapshots during Storybook play int…
Browse files Browse the repository at this point in the history
…eractions (#168)

Co-authored-by: Logan Graham <[email protected]>
  • Loading branch information
omacranger and Logan Graham authored Nov 27, 2024
1 parent 7d3f733 commit 2036ba4
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 24 deletions.
6 changes: 6 additions & 0 deletions visual-js/.changeset/mean-pans-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@saucelabs/visual-storybook": minor
---

add play interaction snapshot testing
re-add storybook 6 support
3 changes: 2 additions & 1 deletion visual-js/visual-storybook/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.work
build/
coverage/
.parent/
.parent/
play.*
32 changes: 21 additions & 11 deletions visual-js/visual-storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"types": "build/index.d.ts",
"license": "MIT",
"files": [
"build"
"build",
"play.*"
],
"type": "module",
"engines": {
Expand All @@ -33,6 +34,10 @@
"require": "./build/config/global-teardown.cjs",
"import": "./build/config/global-teardown.js"
},
"./play": {
"require": "./play.cjs",
"import": "./play.js"
},
"./package.json": "./package.json"
},
"scripts": {
Expand All @@ -44,34 +49,39 @@
"dependencies": {
"@saucelabs/visual": "^0.10.0",
"@saucelabs/visual-playwright": "^0.2.0",
"@storybook/core-events": "^6.4.0 || ^7.0.0 || ^8.0.0",
"@storybook/instrumenter": "^6.4.0 || ^7.0.0 || ^8.0.0",
"@storybook/test-runner": ">=0.13.0",
"exponential-backoff": "^3.1.1",
"jest-playwright-preset": "^2.0.0 || ^3.0.0"
},
"peerDependencies": {
"@storybook/core": "^7.0.0 || ^8.0.0",
"storybook": "^7.0.0 || ^8.0.0"
"storybook": "^6.4.0 || ^7.0.0 || ^8.0.0"
},
"tsup": {
"entry": [
"./src/index.ts",
"./src/config/global-setup.ts",
"./src/config/global-teardown.ts"
],
"entry": {
"build/index": "./src/index.ts",
"build/config/global-setup": "./src/config/global-setup.ts",
"build/config/global-teardown": "./src/config/global-teardown.ts",
"play": "./src/play.ts"
},
"dts": true,
"outDir": "./build",
"outDir": "./",
"format": [
"cjs",
"esm"
],
"external": [
"@saucelabs/visual-storybook"
],
"noExternal": []
"noExternal": [],
"splitting": false
},
"devDependencies": {
"@jest/globals": "^28.0.0 || ^29.0.0",
"@storybook/types": "^8.0.2",
"@storybook/core-events": "^8.4.5",
"@storybook/instrumenter": "^8.4.5",
"@storybook/types": "^8.4.5",
"@tsconfig/node18": "^2.0.0",
"@types/node": "^18.13.0",
"@types/node-fetch": "^2.6.4",
Expand Down
48 changes: 43 additions & 5 deletions visual-js/visual-storybook/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { TestContext } from '@storybook/test-runner';
import { getStoryContext } from '@storybook/test-runner';
import type { Page } from 'playwright-core';
import { internals } from '@saucelabs/visual-playwright';
import { SauceVisualParams, StoryContext, StoryVariation } from './types';
import {
internals,
SauceVisualParams as PlaywrightParams,
} from '@saucelabs/visual-playwright';
import type { SauceVisualParams, StoryContext, StoryVariation } from './types';
import type { Channel } from '@storybook/core/channels';
import events from '@storybook/core/core-events';

import type EventEmitter from 'node:events';

const { VisualPlaywright } = internals;

const clientVersion = 'PKG_VERSION';
Expand Down Expand Up @@ -97,7 +99,7 @@ export const postVisit = async (page: Page, context: TestContext) => {
await page.evaluate(
({ variation, events, storyId }) => {
// @ts-expect-error Global managed by Storybook.
const channel: EventEmitter = globalThis.__STORYBOOK_ADDONS_CHANNEL__;
const channel: Channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__;
if (!channel) {
throw new Error(
'The test runner could not access the Storybook channel. Are you sure the Storybook is running correctly in that URL?',
Expand Down Expand Up @@ -143,3 +145,39 @@ export const postVisit = async (page: Page, context: TestContext) => {
* `postVisit` exported from this package instead.
*/
export const postRender = postVisit;

/**
* Playwright throws an exception if attempting to expose the same binding twice and does not
* expose a way for us to see if something has already been bound. Since we're only given access
* to the Page object during postVisit (not during setup) we can't ensure that it's only added once.
* This is just a simple check to see if the current instance has already been bound and skip
* double binding if so.
*/
let hasExposed = false;

/**
* Used in Storybook's test runner config file (test-runner.js/ts) for the `preVisit` hook. Preps
* the binding for taking visual snapshots during and after render / execution.
*/
export const preVisit = async (page: Page, context: TestContext) => {
if (hasExposed) {
return;
}

await page.exposeBinding(
'takeVisualSnapshot',
async (source, ...args: [string, PlaywrightParams | undefined]) => {
const [name, params] = args;

const storyContext = await getStoryContext(source.page, context);

await takeScreenshot(
augmentStoryName(storyContext, {
name,
}),
params,
);
},
);
hasExposed = true;
};
2 changes: 1 addition & 1 deletion visual-js/visual-storybook/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { postRender, postVisit } from './api';
export { postRender, postVisit, preVisit } from './api';
export { getVisualTestConfig } from './config';
export type { SauceVisualParams, ArgsTypes, StoryVariation } from './types';
42 changes: 42 additions & 0 deletions visual-js/visual-storybook/src/play.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { SauceVisualParams as PlaywrightParams } from '@saucelabs/visual-playwright';
import { instrument } from '@storybook/instrumenter';

const _takeVisualSnapshot = async (
name: string,
params?: PlaywrightParams,
): Promise<void> => {
/**
* @see https://github.com/storybookjs/test-runner?tab=readme-ov-file#storybooktestrunner-user-agent
*/
const isTestRunner = window.navigator.userAgent.match(/StorybookTestRunner/);

if (!isTestRunner) {
console.info(
'Skipping Sauce Visual snapshot -- not in test runner context.',
);
return;
}
if (!window.takeVisualSnapshot) {
throw new Error(
'`takeVisualSnapshot` is not available. Did you setup your `preVisit` hook for Sauce Labs in your Storybook test-runner.js/ts configuration file?',
);
}
await window.takeVisualSnapshot(name, params);
};

export const {
/**
* Takes a screenshot with Sauce Visual. Designed to be used only within the Storybook Test Runner
* execution. Is noop when not in the test runner.
* @param name
* @param params
*/
takeVisualSnapshot,
} = instrument(
{
takeVisualSnapshot: _takeVisualSnapshot,
},
{
intercept: true,
},
);
4 changes: 2 additions & 2 deletions visual-js/visual-storybook/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SauceRegion } from '@saucelabs/visual';
import { SauceVisualParams as PlaywrightParams } from '@saucelabs/visual-playwright';
import type { SauceRegion } from '@saucelabs/visual';
import type { SauceVisualParams as PlaywrightParams } from '@saucelabs/visual-playwright';

export interface VisualOpts extends PlaywrightParams {
user: string | undefined;
Expand Down
9 changes: 8 additions & 1 deletion visual-js/visual-storybook/src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/* eslint-disable no-var */
import { getApi } from '@saucelabs/visual';
import type { getApi } from '@saucelabs/visual';
import type { SauceVisualParams as PlaywrightParams } from '@saucelabs/visual-playwright';

declare global {
var visualApi: ReturnType<typeof getApi>;
var buildId: string;
interface Window {
takeVisualSnapshot?: (
name: string,
opts?: PlaywrightParams,
) => Promise<void>;
}
}
71 changes: 68 additions & 3 deletions visual-js/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3066,8 +3066,10 @@ __metadata:
"@jest/globals": ^28.0.0 || ^29.0.0
"@saucelabs/visual": ^0.10.0
"@saucelabs/visual-playwright": ^0.2.0
"@storybook/core-events": ^8.4.5
"@storybook/instrumenter": ^8.4.5
"@storybook/test-runner": ">=0.13.0"
"@storybook/types": ^8.0.2
"@storybook/types": ^8.4.5
"@tsconfig/node18": ^2.0.0
"@types/node": ^18.13.0
"@types/node-fetch": ^2.6.4
Expand All @@ -3090,8 +3092,7 @@ __metadata:
tsup: ^7.2.0
typescript: ^5.0.4
peerDependencies:
"@storybook/core": ^7.0.0 || ^8.0.0
storybook: ^7.0.0 || ^8.0.0
storybook: ^6.4.0 || ^7.0.0 || ^8.0.0
languageName: unknown
linkType: soft

Expand Down Expand Up @@ -3231,6 +3232,15 @@ __metadata:
languageName: node
linkType: hard

"@storybook/core-events@npm:^8.4.5":
version: 8.4.5
resolution: "@storybook/core-events@npm:8.4.5"
peerDependencies:
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
checksum: d7322c6d8723f98b7a0caf897048f53e4bacfbeb3ebb53a215b42a92222f1f8af4749bede2358af06973bf8e64df874299f342ab30747685c486d1d5d8b8cd28
languageName: node
linkType: hard

"@storybook/core@npm:8.3.5":
version: 8.3.5
resolution: "@storybook/core@npm:8.3.5"
Expand Down Expand Up @@ -3270,6 +3280,25 @@ __metadata:
languageName: node
linkType: hard

"@storybook/global@npm:^5.0.0":
version: 5.0.0
resolution: "@storybook/global@npm:5.0.0"
checksum: ede0ad35ec411fe31c61150dbd118fef344d1d0e72bf5d3502368e35cf68126f6b7ae4a0ab5e2ffe2f0baa3b4286f03ad069ba3e098e1725449ef08b7e154ba8
languageName: node
linkType: hard

"@storybook/instrumenter@npm:^8.4.5":
version: 8.4.5
resolution: "@storybook/instrumenter@npm:8.4.5"
dependencies:
"@storybook/global": ^5.0.0
"@vitest/utils": ^2.1.1
peerDependencies:
storybook: ^8.4.5
checksum: 14093e36b14871e74331074b7261f9103ddfe1233dffa7f722a8e36c955cb5812d7e3a202b8a7673dbdceb10853d0989cef1ff8e928b058635b2032188905842
languageName: node
linkType: hard

"@storybook/preview-api@npm:^8.0.0":
version: 8.3.5
resolution: "@storybook/preview-api@npm:8.3.5"
Expand Down Expand Up @@ -3320,6 +3349,15 @@ __metadata:
languageName: node
linkType: hard

"@storybook/types@npm:^8.4.5":
version: 8.4.5
resolution: "@storybook/types@npm:8.4.5"
peerDependencies:
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
checksum: 9c6d8aef05a475ed42faa63618ee6a958803115fb124281d2724576935630c769166ea5ea3dcabe69a28658d7f190ea3acffbeddd9c0aeb0894688df2ed4fe40
languageName: node
linkType: hard

"@swc/core-darwin-arm64@npm:1.7.35":
version: 1.7.35
resolution: "@swc/core-darwin-arm64@npm:1.7.35"
Expand Down Expand Up @@ -4105,6 +4143,15 @@ __metadata:
languageName: node
linkType: hard

"@vitest/pretty-format@npm:2.1.6":
version: 2.1.6
resolution: "@vitest/pretty-format@npm:2.1.6"
dependencies:
tinyrainbow: ^1.2.0
checksum: 4cab9152ac97fa190db85bbe7e1ae8f1b5d2312fa3ccf7e813119933b2aaf4c763c6156a6d91cb186d3ed4be81f5bb70da2c731fde2d12457fe0871087d2be74
languageName: node
linkType: hard

"@vitest/snapshot@npm:^2.0.3":
version: 2.1.3
resolution: "@vitest/snapshot@npm:2.1.3"
Expand All @@ -4116,6 +4163,17 @@ __metadata:
languageName: node
linkType: hard

"@vitest/utils@npm:^2.1.1":
version: 2.1.6
resolution: "@vitest/utils@npm:2.1.6"
dependencies:
"@vitest/pretty-format": 2.1.6
loupe: ^3.1.2
tinyrainbow: ^1.2.0
checksum: 8b9c994eccb724d76128e875e8438d519bfae0126e7431e8682e7f07d9faeff929db1afa2742b188883e42508d4cdcb2326f9ae27c1b53b5f746d283a9e75462
languageName: node
linkType: hard

"@wdio/config@npm:8.10.4":
version: 8.10.4
resolution: "@wdio/config@npm:8.10.4"
Expand Down Expand Up @@ -10703,6 +10761,13 @@ __metadata:
languageName: node
linkType: hard

"loupe@npm:^3.1.2":
version: 3.1.2
resolution: "loupe@npm:3.1.2"
checksum: 4a75bbe8877a1ced3603e08b1095cd6f4c987c50fe63719fdc3009029560f91e07a915e7f6eff1322bb62bfb2a2beeef06b13ccb3c12f81bda9f3674434dcab9
languageName: node
linkType: hard

"lower-case-first@npm:^2.0.2":
version: 2.0.2
resolution: "lower-case-first@npm:2.0.2"
Expand Down

0 comments on commit 2036ba4

Please sign in to comment.