Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: use storyshot-puppeteer for things other than image snapshots #8934

Merged
merged 10 commits into from Nov 28, 2019
32 changes: 32 additions & 0 deletions .github/workflows/tests-puppeteer.yml
@@ -0,0 +1,32 @@
name: Puppeteer & A11y tests

on: [push]

jobs:
build:

name: Puppeteer & A11y tests
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v1
- name: Cache node modules
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.OS }}-build-${{ env.cache-name }}-
${{ runner.OS }}-build-
${{ runner.OS }}-
- name: install, bootstrap
run: |
yarn bootstrap --core
- name: build storybook
run: |
yarn --cwd examples/official-storybook build-storybook
- name: test
run: |
yarn test --puppeteer
2 changes: 2 additions & 0 deletions addons/a11y/README.md
Expand Up @@ -47,6 +47,8 @@ export const inaccessible = () => (
);
```

## Parameters

For more customizability use parameters to configure [aXe options](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axeconfigure).
You can override these options [at story level too](https://storybook.js.org/docs/configurations/options-parameter/#per-story-options).

Expand Down
2 changes: 1 addition & 1 deletion addons/storyshots/README.md
@@ -1,4 +1,4 @@
# StoryShots

- [addon-storyshots](storyshots-core) - Basic StoryShots api
- [addon-storyshots-puppeteer](storyshots-puppeteer) - Image Snapshots addition to StoryShots based on [puppeteer](https://github.com/GoogleChrome/puppeteer)
- [addon-storyshots-puppeteer](storyshots-puppeteer) - Integration of StoryShots with [puppeteer](https://github.com/GoogleChrome/puppeteer)
249 changes: 145 additions & 104 deletions addons/storyshots/storyshots-puppeteer/README.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion addons/storyshots/storyshots-puppeteer/package.json
Expand Up @@ -29,6 +29,7 @@
"prepare": "node ../../../scripts/prepare.js"
},
"dependencies": {
"@hypnosphi/jest-puppeteer-axe": "^1.4.0",
"@storybook/node-logger": "5.3.0-beta.11",
"@storybook/router": "5.3.0-beta.11",
"@types/jest-image-snapshot": "^2.8.0",
Expand All @@ -40,7 +41,7 @@
"@types/puppeteer": "^2.0.0"
},
"peerDependencies": {
"@storybook/addon-storyshots": "5.3.0-beta.7",
"@storybook/addon-storyshots": "5.3.0-beta.11",
"puppeteer": "^1.12.2 || ^2.0.0"
},
"publishConfig": {
Expand Down
21 changes: 0 additions & 21 deletions addons/storyshots/storyshots-puppeteer/src/ImageSnapshotConfig.ts

This file was deleted.

23 changes: 23 additions & 0 deletions addons/storyshots/storyshots-puppeteer/src/axeTest.ts
@@ -0,0 +1,23 @@
import '@hypnosphi/jest-puppeteer-axe';
import { defaultCommonConfig, CommonConfig } from './config';
import { puppeteerTest } from './puppeteerTest';

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace,no-redeclare
namespace jest {
interface Matchers<R, T> {
toPassAxeTests(parameters: any): R;
}
}
}

export const axeTest = (customConfig: Partial<CommonConfig> = {}) =>
puppeteerTest({
...defaultCommonConfig,
...customConfig,
async testBody(page, options) {
const parameters = options.context.parameters.a11y;
const include = parameters?.element ?? '#root';
await expect(page).toPassAxeTests({ ...parameters, include });
},
});
78 changes: 78 additions & 0 deletions addons/storyshots/storyshots-puppeteer/src/config.ts
@@ -0,0 +1,78 @@
import { MatchImageSnapshotOptions } from 'jest-image-snapshot';
import { Base64ScreenShotOptions, Browser, DirectNavigationOptions, Page } from 'puppeteer';

export interface Context {
kind: string;
story: string;
parameters: {
[key: string]: any;
};
}

interface Options {
context: Context;
url: string;
}

export interface CommonConfig {
storybookUrl: string;
chromeExecutablePath: string;
getGotoOptions: (options: Options) => DirectNavigationOptions;
customizePage: (page: Page) => Promise<void>;
getCustomBrowser: () => Promise<Browser>;
setupTimeout: number;
testTimeout: number;
}

export interface PuppeteerTestConfig extends CommonConfig {
testBody: ((page: Page, options: Options) => void | Promise<void>) & {
filter?: (options: Options) => boolean;
};
}

export interface ImageSnapshotConfig extends CommonConfig {
getMatchOptions: (options: Options) => MatchImageSnapshotOptions;
getScreenshotOptions: (options: Options) => Base64ScreenShotOptions;
beforeScreenshot: (page: Page, options: Options) => void;
afterScreenshot: (options: { image: string; context: Context }) => void;
}

const noop: () => undefined = () => undefined;
const asyncNoop: () => Promise<undefined> = async () => undefined;

export const defaultCommonConfig: CommonConfig = {
storybookUrl: 'http://localhost:6006',
chromeExecutablePath: undefined,
getGotoOptions: noop,
customizePage: asyncNoop,
getCustomBrowser: undefined,
setupTimeout: 15000,
testTimeout: 15000,
};

const getTestBody = (options: Options) => options.context.parameters.puppeteerTest;

function defaultTestBody(page: Page, options: Options) {
const testBody = getTestBody(options);
if (testBody != null) {
return testBody(page, options);
}
return null;
}

defaultTestBody.filter = (options: Options) => getTestBody(options) != null;

export const defaultPuppeteerTestConfig: PuppeteerTestConfig = {
...defaultCommonConfig,
testBody: defaultTestBody,
};

// We consider taking the full page is a reasonable default.
const defaultScreenshotOptions = () => ({ fullPage: true, encoding: 'base64' } as const);
export const defaultImageSnapshotConfig: ImageSnapshotConfig = {
...defaultCommonConfig,
getMatchOptions: noop,
getScreenshotOptions: defaultScreenshotOptions,
beforeScreenshot: noop,
afterScreenshot: noop,
};
123 changes: 15 additions & 108 deletions addons/storyshots/storyshots-puppeteer/src/imageSnapshot.ts
@@ -1,114 +1,21 @@
import { Browser, Page } from 'puppeteer';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
import { logger } from '@storybook/node-logger';
import { constructUrl } from './url';
import { ImageSnapshotConfig } from './ImageSnapshotConfig';
import { defaultImageSnapshotConfig, ImageSnapshotConfig } from './config';
import { puppeteerTest } from './puppeteerTest';

expect.extend({ toMatchImageSnapshot });

// We consider taking the full page is a reasonable default.
const defaultScreenshotOptions = () => ({ fullPage: true, encoding: 'base64' } as const);

const noop: () => undefined = () => undefined;
const asyncNoop: () => Promise<undefined> = async () => undefined;

const defaultConfig: ImageSnapshotConfig = {
storybookUrl: 'http://localhost:6006',
chromeExecutablePath: undefined,
getMatchOptions: noop,
getScreenshotOptions: defaultScreenshotOptions,
beforeScreenshot: noop,
afterScreenshot: noop,
getGotoOptions: noop,
customizePage: asyncNoop,
getCustomBrowser: undefined,
setupTimeout: 15000,
testTimeout: 15000,
};

export const imageSnapshot = (customConfig: Partial<ImageSnapshotConfig> = {}) => {
const {
storybookUrl,
chromeExecutablePath,
getMatchOptions,
getScreenshotOptions,
beforeScreenshot,
afterScreenshot,
getGotoOptions,
customizePage,
getCustomBrowser,
setupTimeout,
testTimeout,
} = { ...defaultConfig, ...customConfig };

let browser: Browser; // holds ref to browser. (ie. Chrome)
let page: Page; // Hold ref to the page to screenshot.

const testFn = async ({ context }: any) => {
const { kind, framework, name } = context;
if (framework === 'react-native') {
// Skip tests since we de not support RN image snapshots.
logger.error(
"It seems you are running imageSnapshot on RN app and it's not supported. Skipping test."
);

return;
}
const url = constructUrl(storybookUrl, kind, name);

if (!browser || !page) {
logger.error(
`Error when generating image snapshot for test ${kind} - ${name} : It seems the headless browser is not running.`
);

throw new Error('no-headless-browser-running');
}

expect.assertions(1);

let image;
try {
await customizePage(page);
await page.goto(url, getGotoOptions({ context, url }));
await beforeScreenshot(page, { context, url });
image = await page.screenshot(getScreenshotOptions({ context, url }));
await afterScreenshot({ image, context });
} catch (e) {
logger.error(
`Error when connecting to ${url}, did you start or build the storybook first? A storybook instance should be running or a static version should be built when using image snapshot feature.`
);
throw e;
}

expect(image).toMatchImageSnapshot(getMatchOptions({ context, url }));
};
testFn.timeout = testTimeout;

testFn.afterAll = async () => {
if (getCustomBrowser && page) {
await page.close();
} else if (browser) {
await browser.close();
}
};

const beforeAll = async () => {
if (getCustomBrowser) {
browser = await getCustomBrowser();
} else {
// eslint-disable-next-line global-require
const puppeteer = require('puppeteer');
// add some options "no-sandbox" to make it work properly on some Linux systems as proposed here: https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322851507
browser = await puppeteer.launch({
args: ['--no-sandbox ', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
executablePath: chromeExecutablePath,
});
}

page = await browser.newPage();
};
beforeAll.timeout = setupTimeout;
testFn.beforeAll = beforeAll;

return testFn;
const config = { ...defaultImageSnapshotConfig, ...customConfig };
const { getMatchOptions, getScreenshotOptions, beforeScreenshot, afterScreenshot } = config;

return puppeteerTest({
...config,
async testBody(page, options) {
expect.assertions(1);
await beforeScreenshot(page, options);
const image = await page.screenshot(getScreenshotOptions(options));
await afterScreenshot({ image, context: options.context });
expect(image).toMatchImageSnapshot(getMatchOptions(options));
},
});
};
4 changes: 3 additions & 1 deletion addons/storyshots/storyshots-puppeteer/src/index.ts
@@ -1,2 +1,4 @@
export * from './ImageSnapshotConfig';
export * from './config';
export * from './puppeteerTest';
export * from './axeTest';
export * from './imageSnapshot';