Skip to content

Commit

Permalink
[Storybook] Add ability to auto capture variations of a Story (#163)
Browse files Browse the repository at this point in the history
Co-authored-by: Logan Graham <[email protected]>
  • Loading branch information
omacranger and Logan Graham authored Nov 19, 2024
1 parent e995ec3 commit 7fcad30
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 12 deletions.
5 changes: 5 additions & 0 deletions visual-js/.changeset/tender-turkeys-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@saucelabs/visual-storybook": minor
---

add ability to test variations of a story using overridable args
1 change: 1 addition & 0 deletions visual-js/visual-storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"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"
},
"tsup": {
Expand Down
112 changes: 102 additions & 10 deletions visual-js/visual-storybook/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ 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 } from './types';
import { SauceVisualParams, StoryContext, StoryVariation } from './types';
import events from '@storybook/core/core-events';

import type EventEmitter from 'node:events';

const { VisualPlaywright } = internals;

Expand All @@ -13,20 +16,19 @@ export const VisualStorybook = new VisualPlaywright(
);

/**
* Used in Storybook's test runner config file (test-runner.js/ts) for the `postVisit` hook. Takes
* a snapshot of the current viewport and uploads it to Sauce Visual.
* Proxy Storybook information to take Playwright snapshots with the appropriate metadata. Internal.
* @param context
* @param params
*/
export const postVisit = async (page: Page, context: TestContext) => {
const storyContext = await getStoryContext(page, context);
const sauceVisualParams = storyContext.parameters.sauceVisual as
| Partial<SauceVisualParams>
| undefined;
const takeScreenshot = async (
context: StoryContext,
params: Partial<SauceVisualParams> | undefined,
) => {
const {
clip: shouldClip = true,
clipSelector = '#storybook-root',
...otherOptions
} = sauceVisualParams ?? {};

} = params ?? {};
await VisualStorybook.takePlaywrightScreenshot(
page,
{
Expand All @@ -43,6 +45,96 @@ export const postVisit = async (page: Page, context: TestContext) => {
);
};

/**
* Augment story names when creating visual variations.
* @param context
* @param variation
*/
export const augmentStoryName = (
context: StoryContext,
variation: StoryVariation,
): StoryContext => {
const { id, title } = context;
let { name } = context;

if (variation.name) {
name = variation.name;
}

if (variation.prefix) {
name = `${variation.prefix}${name}`;
}

if (variation.postfix) {
name = `${variation.postfix}${name}`;
}

return {
id,
title,
name,
};
};

/**
* Used in Storybook's test runner config file (test-runner.js/ts) for the `postVisit` hook. Takes
* a snapshot of the current viewport and uploads it to Sauce Visual.
*/
export const postVisit = async (page: Page, context: TestContext) => {
const storyContext = await getStoryContext(page, context);
const sauceVisualParams = storyContext.parameters.sauceVisual as
| Partial<SauceVisualParams>
| undefined;
const variations = sauceVisualParams?.variations;

await takeScreenshot(storyContext, sauceVisualParams);

if (variations) {
const storyId = storyContext.id;
const args = storyContext.initialArgs;

for (const variation of variations) {
await page.evaluate(
({ variation, events, storyId }) => {
// @ts-expect-error Global managed by Storybook.
const channel: EventEmitter = 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?',
);
}

const { args } = variation;

// Reset values to default
channel.emit(events['RESET_STORY_ARGS'], { storyId });

// Then update them with the requested values
if (args) {
channel.emit(events['UPDATE_STORY_ARGS'], {
storyId,
updatedArgs: {
...args,
},
});
}
},
{
variation,
events,
storyId,
args,
},
);

await takeScreenshot(
augmentStoryName(storyContext, variation),
sauceVisualParams,
);
}
}
};

/**
* Exported for backwards compatibility. Replaced by `postVisit` in `@storybook/test-runner` version 0.15.0 and up. Use
* `postVisit` exported from this package instead.
Expand Down
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 { getVisualTestConfig } from './config';
export type { SauceVisualParams } from './types';
export type { SauceVisualParams, ArgsTypes, StoryVariation } from './types';
36 changes: 35 additions & 1 deletion visual-js/visual-storybook/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,43 @@ export interface VisualOpts extends PlaywrightParams {
customId: string | null;
}

export interface SauceVisualParams extends PlaywrightParams {
export interface ArgsTypes {
[key: string]: any;
}

export interface StoryVariation<Args = ArgsTypes> {
/**
* A string to prepend prior to the story name when taking a snapshot.
*/
prefix?: string;
/**
* A string to append after the story name when taking a snapshot.
*/
postfix?: string;
/**
* One or more Args to overwrite in your Storybook stories. Will take a screenshot for each
* requested variation and upload it to Sauce Labs.
*/
args?: Args;
/**
* A name to optionally use instead of the default story name.
*/
name?: string;
}

export interface SauceVisualParams<Args = ArgsTypes> extends PlaywrightParams {
/**
* Whether we should clip the story reduce whitespaces in snapshots.
*/
clip?: boolean;
/**
* Variations of the current story we should take in addition to the default state.
*/
variations?: StoryVariation<Args>[];
}

export interface StoryContext {
id: string;
title: string;
name: string;
}
1 change: 1 addition & 0 deletions visual-js/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3090,6 +3090,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
languageName: unknown
linkType: soft
Expand Down

0 comments on commit 7fcad30

Please sign in to comment.