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

Release 0.9.4 #258

Merged
merged 3 commits into from Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 17 additions & 8 deletions README.md
Expand Up @@ -97,12 +97,13 @@ yarn storybook
yarn test-storybook
```

> **NOTE:** The runner assumes that your Storybook is running on port `6006`. If you're running Storybook in another port, either use --url or set the TARGET_URL before running your command like:
> **Note**
> The runner assumes that your Storybook is running on port `6006`. If you're running Storybook in another port, either use --url or set the TARGET_URL before running your command like:
>
> ```jsx
> yarn test-storybook --url http://localhost:9009
> yarn test-storybook --url http://127.0.0.1:9009
> or
> TARGET_URL=http://localhost:9009 yarn test-storybook
> TARGET_URL=http://127.0.0.1:9009 yarn test-storybook
> ```

## CLI Options
Expand Down Expand Up @@ -138,6 +139,9 @@ Usage: test-storybook [options]

The test runner is based on [Jest](https://jestjs.io/) and will accept most of the [CLI options](https://jestjs.io/docs/cli) that Jest does, like `--watch`, `--watchAll`, `--maxWorkers`, etc. It works out of the box, but if you want better control over its configuration, you can eject its configuration by running `test-storybook --eject` to create a local `test-runner-jest.config.js` file in the root folder of your project. This file will be used by the test runner.

> **Note**
> The `test-runner-jest.config.js` file can be placed inside of your Storybook config dir as well. If you pass the `--config-dir` option, the test-runner will look for the config file there as well.

The configuration file will accept options for two runners:

#### Jest-playwright options
Expand Down Expand Up @@ -232,7 +236,8 @@ If you are running tests against a local Storybook but for some reason want to r
yarn test-storybook --index-json
```

> **NOTE:** index.json mode is not compatible with watch mode.
> **Note**
> index.json mode is not compatible with watch mode.

## Running in CI

Expand Down Expand Up @@ -265,7 +270,8 @@ jobs:
TARGET_URL: '${{ github.event.deployment_status.target_url }}'
```

> **_NOTE:_** If you're running the test-runner against a `TARGET_URL` of a remotely deployed Storybook (e.g. Chromatic), make sure that the URL loads a publicly available Storybook. Does it load correctly when opened in incognito mode on your browser? If your deployed Storybook is private and has authentication layers, the test-runner will hit them and thus not be able to access your stories. If that is the case, use the next option instead.
> **Note**
> If you're running the test-runner against a `TARGET_URL` of a remotely deployed Storybook (e.g. Chromatic), make sure that the URL loads a publicly available Storybook. Does it load correctly when opened in incognito mode on your browser? If your deployed Storybook is private and has authentication layers, the test-runner will hit them and thus not be able to access your stories. If that is the case, use the next option instead.

### 2. Running against locally built Storybooks in CI

Expand Down Expand Up @@ -297,7 +303,8 @@ jobs:
run: yarn test-storybook:ci
```

> **_NOTE:_** Building Storybook locally makes it simple to test Storybooks that could be available remotely, but are under authentication layers. If you also deploy your Storybooks somewhere (e.g. Chromatic, Vercel, etc.), the Storybook URL can still be useful with the test-runner. You can pass it to the `REFERENCE_URL` environment variable when running the test-storybook command, and if a story fails, the test-runner will provide a helpful message with the link to the story in your published Storybook instead.
> **Note**
> Building Storybook locally makes it simple to test Storybooks that could be available remotely, but are under authentication layers. If you also deploy your Storybooks somewhere (e.g. Chromatic, Vercel, etc.), the Storybook URL can still be useful with the test-runner. You can pass it to the `REFERENCE_URL` environment variable when running the test-storybook command, and if a story fails, the test-runner will provide a helpful message with the link to the story in your published Storybook instead.

## Setting up code coverage

Expand Down Expand Up @@ -389,7 +396,8 @@ Here's an example on how to achieve that:
}
```

> NOTE: If your other tests (e.g. Jest) are using a different coverageProvider than `babel`, you will have issue when merging the coverage files. [More info here](#merging-test-coverage-results-in-wrong-coverage).
> **Note**
> If your other tests (e.g. Jest) are using a different coverageProvider than `babel`, you will have issues when merging the coverage files. [More info here](#merging-test-coverage-results-in-wrong-coverage).

## Experimental test hook API

Expand All @@ -403,7 +411,8 @@ The render functions are async functions that receive a [Playwright Page](https:

All three functions can be set up in the configuration file `.storybook/test-runner.js` which can optionally export any of these functions.

> **NOTE:** These test hooks are experimental and may be subject to breaking changes. We encourage you to test as much as possible within the story's play function.
> **Note**
> These test hooks are experimental and may be subject to breaking changes. We encourage you to test as much as possible within the story's play function.

### DOM snapshot recipe

Expand Down
27 changes: 22 additions & 5 deletions bin/test-storybook.js
Expand Up @@ -13,6 +13,14 @@ const { getCliOptions } = require('../dist/cjs/util/getCliOptions');
const { getStorybookMetadata } = require('../dist/cjs/util/getStorybookMetadata');
const { transformPlaywrightJson } = require('../dist/cjs/playwright/transformPlaywrightJson');

const glob_og = require('glob');

const glob = function (pattern, options) {
return new Promise((resolve, reject) => {
glob_og(pattern, options, (err, files) => (err === null ? resolve(files) : reject(err)));
});
};

// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'test';
process.env.NODE_ENV = 'test';
Expand Down Expand Up @@ -114,9 +122,18 @@ async function executeJestPlaywright(args) {
const jest = require(jestPath);
let argv = args.slice(2);

const jestConfigPath = fs.existsSync('test-runner-jest.config.js')
? 'test-runner-jest.config.js'
: path.resolve(__dirname, '../playwright/test-runner-jest.config.js');
// jest configs could either come in the root dir, or inside of the Storybook config dir
const configDir = process.env.STORYBOOK_CONFIG_DIR || '';
const [userDefinedJestConfig] = (
await Promise.all([
glob(path.join(configDir, 'test-runner-jest*')),
glob(path.join('test-runner-jest*')),
])
).reduce((a, b) => a.concat(b), []);

const jestConfigPath =
userDefinedJestConfig ||
path.resolve(__dirname, path.join('..', 'playwright', 'test-runner-jest.config.js'));

argv.push('--config', jestConfigPath);

Expand All @@ -133,7 +150,7 @@ async function checkStorybook(url) {

If you're not running Storybook on the default 6006 port or want to run the tests against any custom URL, you can pass the --url flag like so:

yarn test-storybook --url http://localhost:9009
yarn test-storybook --url http://127.0.0.1:9009

More info at https://github.com/storybookjs/test-runner#getting-started`
);
Expand Down Expand Up @@ -222,7 +239,7 @@ const main = async () => {
// set this flag to skip reporting coverage in watch mode
isWatchMode = jestOptions.watch || jestOptions.watchAll;

const rawTargetURL = process.env.TARGET_URL || runnerOptions.url || 'http://localhost:6006';
const rawTargetURL = process.env.TARGET_URL || runnerOptions.url || 'http://127.0.0.1:6006';
await checkStorybook(rawTargetURL);

const targetURL = sanitizeURL(rawTargetURL);
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -116,6 +116,7 @@
"can-bind-to-host": "^1.1.1",
"commander": "^9.0.0",
"expect-playwright": "^0.8.0",
"glob": "^8.1.0",
"jest": "^28.0.0",
"jest-environment-node": "^28.0.0",
"jest-circus": "^28.0.0",
Expand Down
142 changes: 120 additions & 22 deletions src/setup-page.ts
Expand Up @@ -63,39 +63,137 @@ export const setupPage = async (page: Page) => {
const red = (message) => \`\\u001b[31m\${message}\\u001b[39m\`;
const yellow = (message) => \`\\u001b[33m\${message}\\u001b[39m\`;

// removes circular references from the object
function serializer(replacer, cycleReplacer) {
let stack = [],
keys = [];

if (cycleReplacer == null)
cycleReplacer = function (_key, value) {
if (stack[0] === value) return '[Circular]';
return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']';
};
// Code taken and adjusted from https://github.com/davidmarkclements/fast-safe-stringify
var LIMIT_REPLACE_NODE = '[...]'
var CIRCULAR_REPLACE_NODE = '[Circular]'

var arr = []
var replacerStack = []

function defaultOptions () {
return {
depthLimit: Number.MAX_SAFE_INTEGER,
edgesLimit: Number.MAX_SAFE_INTEGER
}
}

return function (key, value) {
if (stack.length > 0) {
let thisPos = stack.indexOf(this);
~thisPos ? stack.splice(thisPos + 1) : stack.push(this);
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value);
// Regular stringify
function stringify (obj, replacer, spacer, options) {
if (typeof options === 'undefined') {
options = defaultOptions()
}

decirc(obj, '', 0, [], undefined, 0, options)
var res
try {
if (replacerStack.length === 0) {
res = JSON.stringify(obj, replacer, spacer)
} else {
stack.push(value);
res = JSON.stringify(obj, replaceGetterValues(replacer), spacer)
}
} catch (_) {
return JSON.stringify('[unable to serialize, circular reference is too complex to analyze]')
} finally {
while (arr.length !== 0) {
var part = arr.pop()
if (part.length === 4) {
Object.defineProperty(part[0], part[1], part[3])
} else {
part[0][part[1]] = part[2]
}
}
}
return res
}

return replacer == null ? value : replacer.call(this, key, value);
};
function setReplace (replace, val, k, parent) {
var propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k)
if (propertyDescriptor.get !== undefined) {
if (propertyDescriptor.configurable) {
Object.defineProperty(parent, k, { value: replace })
arr.push([parent, k, val, propertyDescriptor])
} else {
replacerStack.push([val, k, replace])
}
} else {
parent[k] = replace
arr.push([parent, k, val])
}
}

function safeStringify(obj, replacer, spaces, cycleReplacer) {
return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces);
function decirc (val, k, edgeIndex, stack, parent, depth, options) {
depth += 1
var i
if (typeof val === 'object' && val !== null) {
for (i = 0; i < stack.length; i++) {
if (stack[i] === val) {
setReplace(CIRCULAR_REPLACE_NODE, val, k, parent)
return
}
}

if (
typeof options.depthLimit !== 'undefined' &&
depth > options.depthLimit
) {
setReplace(LIMIT_REPLACE_NODE, val, k, parent)
return
}

if (
typeof options.edgesLimit !== 'undefined' &&
edgeIndex + 1 > options.edgesLimit
) {
setReplace(LIMIT_REPLACE_NODE, val, k, parent)
return
}

stack.push(val)
// Optimize for Arrays. Big arrays could kill the performance otherwise!
if (Array.isArray(val)) {
for (i = 0; i < val.length; i++) {
decirc(val[i], i, i, stack, val, depth, options)
}
} else {
var keys = Object.keys(val)
for (i = 0; i < keys.length; i++) {
var key = keys[i]
decirc(val[key], key, i, stack, val, depth, options)
}
}
stack.pop()
}
}

// wraps replacer function to handle values we couldn't replace
// and mark them as replaced value
function replaceGetterValues (replacer) {
replacer =
typeof replacer !== 'undefined'
? replacer
: function (k, v) {
return v
}
return function (key, val) {
if (replacerStack.length > 0) {
for (var i = 0; i < replacerStack.length; i++) {
var part = replacerStack[i]
if (part[1] === key && part[0] === val) {
val = part[2]
replacerStack.splice(i, 1)
break
}
}
}
return replacer.call(this, key, val)
}
}
// end of fast-safe-stringify code

function composeMessage(args) {
if (typeof args === 'undefined') return "undefined";
if (typeof args === 'string') return args;
return safeStringify(args);
return stringify(args, null, null, { depthLimit: 5, edgesLimit: 100 });
}

function truncate(input, limit) {
Expand Down
2 changes: 1 addition & 1 deletion src/util/getParsedCliOptions.ts
Expand Up @@ -27,7 +27,7 @@ export const getParsedCliOptions = () => {
.option(
'--url <url>',
'Define the URL to run tests in. Useful for custom Storybook URLs',
'http://localhost:6006'
'http://127.0.0.1:6006'
)
.option(
'--maxWorkers <amount>',
Expand Down
11 changes: 11 additions & 0 deletions yarn.lock
Expand Up @@ -6909,6 +6909,17 @@ glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
once "^1.3.0"
path-is-absolute "^1.0.0"

glob@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^5.0.1"
once "^1.3.0"

global-modules@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d"
Expand Down