Giter Site home page Giter Site logo

storybookjs / test-runner Goto Github PK

View Code? Open in Web Editor NEW
210.0 11.0 62.0 5.75 MB

πŸš• Turn stories into executable tests

Home Page: https://storybook.js.org/blog/interaction-testing-with-storybook/

License: MIT License

JavaScript 11.13% TypeScript 84.21% CSS 1.45% Shell 0.04% MDX 3.17%
storybook testing jest test-runner testing-library testing-tools

test-runner's Introduction

Storybook Test Runner

Storybook test runner turns all of your stories into executable tests.

Table of Contents

Features

  • ⚑️ Zero config setup
  • πŸ’¨ Smoke test all stories
  • ▢️ Test stories with play functions
  • πŸƒ Test your stories in parallel in a headless browser
  • πŸ‘· Get feedback from error with a link directly to the story
  • πŸ›Β Debug them visually and interactively in a live browser with addon-interactions
  • 🎭 Powered by Jest and Playwright
  • πŸ‘€Β Watch mode, filters, and the conveniences you'd expect
  • πŸ“”Β Code coverage reports

How it works

See the announcement of Interaction Testing with Storybook in detail in this blog post or watch this video to see it in action.

The Storybook test runner uses Jest as a runner, and Playwright as a testing framework. Each one of your .stories files is transformed into a spec file, and each story becomes a test, which is run in a headless browser.

The test runner is simple in design – it just visits each story from a running Storybook instance and makes sure the component is not failing:

  • For stories without a play function, it verifies whether the story rendered without any errors. This is essentially a smoke test.
  • For those with a play function, it also checks for errors in the play function and that all assertions passed. This is essentially an interaction test.

If there are any failures, the test runner will provide an output with the error, alongside with a link to the failing story, so you can see the error yourself and debug it directly in the browser:

Storybook compatibility

Use the following table to use the correct version of this package, based on the version of Storybook you're using:

Test runner version Storybook version
^0.17.0 ^8.0.0
~0.16.0 ^7.0.0
~0.9.4 ^6.4.0

Getting started

  1. Install the test runner:
yarn add @storybook/test-runner -D
  1. Add a test-storybook script to your package.json
{
  "scripts": {
    "test-storybook": "test-storybook"
  }
}
  1. Optionally, follow the documentation for writing interaction tests and using addon-interactions to visualize the interactions with an interactive debugger in Storybook.

  2. Run Storybook (the test runner runs against a running Storybook instance):

yarn storybook
  1. Run the test runner:
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:

yarn test-storybook --url http://127.0.0.1:9009
or
TARGET_URL=http://127.0.0.1:9009 yarn test-storybook

CLI Options

Usage: test-storybook [options]
Options Description
--help Output usage information
test-storybook --help
-i, --index-json Run in index json mode. Automatically detected (requires a compatible Storybook)
test-storybook --index-json
--no-index-json Disables index json mode
test-storybook --no-index-json
-c, --config-dir [dir-name] Directory where to load Storybook configurations from
test-storybook -c .storybook
--watch Watch files for changes and rerun tests related to changed files.
test-storybook --watch
--watchAll Watch files for changes and rerun all tests when something changes.
test-storybook --watchAll
--coverage Indicates that test coverage information should be collected and reported in the output
test-storybook --coverage
--coverageDirectory Directory where to write coverage report output
test-storybook --coverage --coverageDirectory coverage/ui/storybook
--url Define the URL to run tests in. Useful for custom Storybook URLs
test-storybook --url http://the-storybook-url-here.com
--browsers Define browsers to run tests in. One or multiple of: chromium, firefox, webkit
test-storybook --browsers firefox chromium
--maxWorkers [amount] Specifies the maximum number of workers the worker-pool will spawn for running tests
test-storybook --maxWorkers=2
--no-cache Disable the cache
test-storybook --no-cache
--clearCache Deletes the Jest cache directory and then exits without running tests
test-storybook --clearCache
--verbose Display individual test results with the test suite hierarchy
test-storybook --verbose
-u, --updateSnapshot Use this flag to re-record every snapshot that fails during this test run
test-storybook -u
--eject Creates a local configuration file to override defaults of the test-runner
test-storybook --eject
--json Prints the test results in JSON. This mode will send all other test output and user messages to stderr.
test-storybook --json
--outputFile Write test results to a file when the --json option is also specified.
test-storybook --json --outputFile results.json
--junit Indicates that test information should be reported in a junit file.
test-storybook --**junit**
--ci Instead of the regular behavior of storing a new snapshot automatically, it will fail the test and require Jest to be run with --updateSnapshot.
test-storybook --ci
--shard [shardIndex/shardCount] Splits your test suite across different machines to run in CI.
test-storybook --shard=1/3
--failOnConsole Makes tests fail on browser console errors
test-storybook --failOnConsole
--includeTags (experimental) Only test stories that match the specified tags, comma separated
test-storybook --includeTags="test-only"
--excludeTags (experimental) Do not test stories that match the specified tags, comma separated
test-storybook --excludeTags="broken-story,todo"
--skipTags (experimental) Do not test stories that match the specified tags and mark them as skipped in the CLI output, comma separated
test-storybook --skipTags="design"

Ejecting configuration

The test runner is based on Jest and will accept most of the CLI options 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

The test runner uses jest-playwright and you can pass testEnvironmentOptions to further configure it.

Jest options

The Storybook test runner comes with Jest installed as an internal dependency. You can pass Jest options based on the version of Jest that comes with the test runner.

Test runner version Jest version
^0.6.2 ^26.6.3 or ^27.0.0
^0.7.0 ^28.0.0
^0.14.0 ^29.0.0

If you're already using a compatible version of Jest, the test runner will use it, instead of installing a duplicate version in your node_modules folder.

Here's an example of an ejected file used to extend the tests timeout from Jest:

// ./test-runner-jest.config.js
const { getJestConfig } = require('@storybook/test-runner');

const testRunnerConfig = getJestConfig();

/**
 * @type {import('@jest/types').Config.InitialOptions}
 */
module.exports = {
  // The default Jest configuration comes from @storybook/test-runner
  ...testRunnerConfig,
  /** Add your own overrides below
   * @see https://jestjs.io/docs/configuration
   */
  testTimeout: 20000, // default timeout is 15s
};

Filtering tests (experimental)

You might want to skip certain stories in the test-runner, run tests only against a subset of stories, or exclude certain stories entirely from your tests. This is possible via the tags annotation.

This annotation can be part of a story, therefore only applying to it, or the component meta (the default export), which applies to all stories in the file:

const meta = {
  component: Button,
  tags: ['design', 'test-only'],
};
export default meta;

// will inherit tags from meta with value ['design', 'test-only']
export const Primary = {};

export const Secondary = {
  // will override tags to be just ['skip']
  tags: ['skip'],
};

Note You can't import constants from another file and use them to define tags in your stories. The tags in your stories or meta have to be defined inline, as an array of strings. This is a limitation due to Storybook's static analysis.

Once your stories have your own custom tags, you can filter them via the tags property in your test-runner configuration file. You can also use the CLI flags --includeTags, --excludeTags or --skipTags for the same purpose. The CLI flags will take precedence over the tags in the test-runner config, therefore overriding them.

Both --skipTags and --excludeTags will prevent a story from being tested. The difference is that skipped tests will appear as "skipped" in the cli output, whereas excluded tests will not appear at all. Skipped tests can be useful to indicate tests that are temporarily disabled.

Test reporters

The test runner uses default Jest reporters, but you can add additional reporters by ejecting the configuration as explained above and overriding (or merging with) the reporters property.

Additionally, if you pass --junit to test-storybook, the test runner will add jest-junit to the reporters list and generate a test report in a JUnit XML format. You can further configure the behavior of jest-junit by either setting specific JEST_JUNIT_* environment variables or by defining a jest-junit field in your package.json with the options you want, which will be respected when generating the report. You can look at all available options here: https://github.com/jest-community/jest-junit#configuration

Running against a deployed Storybook

By default, the test runner assumes that you're running it against a locally served Storybook on port 6006. If you want to define a target url so it runs against deployed Storybooks, you can do so by passing the TARGET_URL environment variable:

TARGET_URL=https://the-storybook-url-here.com yarn test-storybook

Or by using the --url flag:

yarn test-storybook --url https://the-storybook-url-here.com

Index.json mode

By default, the test runner transforms your story files into tests. It also supports a secondary "index.json mode" which runs directly against your Storybook's index data, which dependending on your Storybook version is located in a stories.json or index.json, a static index of all the stories.

This is particularly useful for running against a deployed storybook because index.json is guaranteed to be in sync with the Storybook you are testing. In the default, story file-based mode, your local story files may be out of sync – or you might not even have access to the source code.

Furthermore, it is not possible to run the test-runner directly against .mdx stories or custom CSF dialects like when writing Svelte native stories with addon-svelte-csf. In these cases index.json mode must be used.

If you're using Storybook 7.0

To run in index.json mode, first make sure your Storybook has a v4 index.json file. You can find it when navigating to:

https://your-storybook-url-here.com/index.json

It should be a JSON file and the first key should be "v": 4 followed by a key called "entries" containing a map of story IDs to JSON objects.

In Storybok 7.0, index.json is enabled by default, unless you are using the storiesOf() syntax, in which case it is not supported.

On Storybook 6.4 and 6.5, to run in index.json mode, first make sure your Storybook has a file called stories.json that has "v": 3, available at:

https://your-storybook-url-here.com/stories.json

If your Storybook does not have a stories.json file, you can generate one, provided:

  • You are running Storybook 6.4 or above
  • You are not using storiesOf stories

To enable stories.json in your Storybook, set the buildStoriesJson feature flag in .storybook/main.js:

// .storybook/main.ts
const config = {
  // ... rest of the config
  features: { buildStoriesJson: true },
};
export default config;

Once you have a valid stories.json file, your Storybook will be compatible with the "index.json mode".

By default, the test runner will detect whether your Storybook URL is local or remote, and if it is remote, it will run in "index.json mode" automatically. To disable it, you can pass the --no-index-json flag:

yarn test-storybook --no-index-json

If you are running tests against a local Storybook but for some reason want to run in "index.json mode", you can pass the --index-json flag:

yarn test-storybook --index-json

Note index.json mode is not compatible with watch mode.

Running in CI

If you want to add the test-runner to CI, there are a couple of ways to do so:

1. Running against deployed Storybooks on Github Actions deployment

On Github actions, once services like Vercel, Netlify and others do deployment runs, they follow a pattern of emitting a deployment_status event containing the newly generated URL under deployment_status.target_url. You can use that URL and set it as TARGET_URL for the test-runner.

Here's an example of an action to run tests based on that:

name: Storybook Tests
on: deployment_status
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    if: github.event.deployment_status.state == 'success'
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v2
        with:
          node-version: '14.x'
      - name: Install dependencies
        run: yarn
      - name: Run Storybook tests
        run: yarn test-storybook
        env:
          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.

2. Running against locally built Storybooks in CI

In order to build and run tests against your Storybook in CI, you might need to use a combination of commands involving the concurrently, http-server and wait-on libraries. Here's a recipe that does the following: Storybook is built and served locally, and once it is ready, the test runner will run against it.

{
  "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && yarn test-storybook\""
}

And then you can essentially run test-storybook:ci in your CI:

name: Storybook Tests
on: push
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v2
        with:
          node-version: '14.x'
      - name: Install dependencies
        run: yarn
      - name: Run Storybook tests
        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.

Setting up code coverage

The test runner supports code coverage with the --coverage flag or STORYBOOK_COLLECT_COVERAGE environment variable. The pre-requisite is that your components are instrumented using istanbul.

1 - Instrument the code

Instrumenting the code is an important step, which allows lines of code to be tracked by Storybook. This is normally achieved by using instrumentation libraries such as the Istanbul Babel plugin, or its Vite counterpart. In Storybook, you can set up instrumentation in two different ways:

Using @storybook/addon-coverage

For select frameworks (React, Preact, HTML, Web components, Svelte and Vue) you can use the @storybook/addon-coverage addon, which will automatically configure the plugin for you.

Install @storybook/addon-coverage:

yarn add -D @storybook/addon-coverage

And register it in your .storybook/main.js file:

// .storybook/main.ts
const config = {
  // ...rest of your code here
  addons: ['@storybook/addon-coverage'],
};
export default config;

The addon has default options that might suffice for your project, and it accepts an options object for project-specific configuration.

Manually configuring istanbul

If your framework does not use Babel or Vite, such as Angular, you will have to manually configure whatever flavor of Istanbul (Webpack loader, etc.) your project might require. Also, if your project uses Vue or Svelte, you will need to add one extra configuration for nyc.

You can find recipes in this repository that include many different configurations and steps on how to set up coverage in each of them.

2 - Run tests with --coverage flag

After setting up instrumentation, run Storybook then run the test-runner with --coverage:

yarn test-storybook --coverage

The test runner will report the results in the CLI and generate a coverage/storybook/coverage-storybook.json file which can be used by nyc.

Note If your components are not shown in the report and you're using Vue or Svelte, it's probably because you're missing a .nycrc.json file to specify the file extensions. Use the recipes for reference on how to set that up.

If you want to generate coverage reports with different reporters, you can use nyc and point it to the folder which contains the Storybook coverage file. nyc is a dependency of the test runner so you will already have it in your project.

Here's an example generating an lcov report:

npx nyc report --reporter=lcov -t coverage/storybook --report-dir coverage/storybook

This will generate a more detailed, interactive coverage summary that you can access at coverage/storybook/index.html file which can be explored and will show the coverage in detail:

The nyc command will respect nyc configuration files if you have them in your project.

If you want certain parts of your code to be deliberately ignored, you can use istanbul parsing hints.

3 - Merging code coverage with coverage from other tools

The test runner reports coverage related to the coverage/storybook/coverage-storybook.json file. This is by design, showing you the coverage which is tested while running Storybook.

Now, you might have other tests (e.g. unit tests) which are not covered in Storybook but are covered when running tests with Jest, which you might also generate coverage files from, for instance. In such cases, if you are using tools like Codecov to automate reporting, the coverage files will be detected automatically and if there are multiple files in the coverage folder, they will be merged automatically.

Alternatively, in case you want to merge coverages from other tools, you should:

1 - move or copy the coverage/storybook/coverage-storybook.json into coverage/coverage-storybook.json; 2 - run nyc report against the coverage folder.

Here's an example on how to achieve that:

{
  "scripts": {
    "test:coverage": "jest --coverage",
    "test-storybook:coverage": "test-storybook --coverage",
    "coverage-report": "cp coverage/storybook/coverage-storybook.json coverage/coverage-storybook.json && nyc report --reporter=html -t coverage --report-dir 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.

4 - Run tests with --shard flag

The test-runner collects all coverage in one file coverage/storybook/coverage-storybook.json. To split the coverage file you should rename it using the shard-index. To report the coverage you should merge the coverage files with the nyc merge command.

Github CI example:

test:
  name: Running Test-storybook (${{ matrix.shard }})
  strategy:
    matrix:
      shard: [1, 2, 3, 4]
  steps:
    - name: Testing storybook
      run: yarn test-storybook --coverage --shard=${{ matrix.shard }}/${{ strategy.job-total }}
    - name: Renaming coverage file
      run: mv coverage/storybook/coverage-storybook.json coverage/storybook/coverage-storybook-${matrix.shard}.json
report-coverage:
  name: Reporting storybook coverage
  steps:
    - name: Merging coverage
      run: yarn nyc merge coverage/storybook merged-output/merged-coverage.json
    - name: Report coverage
      run: yarn nyc report --reporter=text -t merged-output --report-dir merged-output

Circle CI example:

test:
  parallelism: 4
  steps:
    - run:
        command: yarn test-storybook --coverage --shard=$(expr $CIRCLE_NODE_INDEX + 1)/$CIRCLE_NODE_TOTAL
        command: mv coverage/storybook/coverage-storybook.json coverage/storybook/coverage-storybook-${CIRCLE_NODE_INDEX + 1}.json
report-coverage:
  steps:
    - run:
        command: yarn nyc merge coverage/storybook merged-output/merged-coverage.json
        command: yarn nyc report --reporter=text -t merged-output --report-dir merged-output

Gitlab CI example:

test:
  parallel: 4
  script:
    - yarn test-storybook --coverage --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
    - mv coverage/storybook/coverage-storybook.json coverage/storybook/coverage-storybook-${CI_NODE_INDEX}.json
report-coverage:
  script:
    - yarn nyc merge coverage/storybook merged-output/merged-coverage.json
    - yarn nyc report --reporter=text -t merged-output --report-dir merged-output

Test hooks API

The test runner renders a story and executes its play function if one exists. However, there are certain behaviors that are not possible to achieve via the play function, which executes in the browser. For example, if you want the test runner to take visual snapshots for you, this is something that is possible via Playwright/Jest, but must be executed in Node.

To enable use cases like visual or DOM snapshots, the test runner exports test hooks that can be overridden globally. These hooks give you access to the test lifecycle before and after the story is rendered.

There are three hooks: setup, preVisit, and postVisit. setup executes once before all the tests run. preVisit and postVisit execute within a test before and after a story is rendered.

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

Note The preVisit and postVisit functions will be executed for all stories.

setup

Async function that executes once before all the tests run. Useful for setting node-related configuration, such as extending Jest global expect for accessibility matchers.

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async setup() {
    // execute whatever you like, in Node, once before all tests run
  },
};
export default config;

preRender (deprecated)

Note This hook is deprecated. It has been renamed to preVisit, please use it instead.

preVisit

Async function that receives a Playwright Page and a context object with the current story's id, title, and name. Executes within a test before the story is rendered. Useful for configuring the Page before the story renders, such as setting up the viewport size.

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async preVisit(page, context) {
    // execute whatever you like, before the story renders
  },
};
export default config;

postRender (deprecated)

Note This hook is deprecated. It has been renamed to postVisit, please use it instead.

postVisit

Async function that receives a Playwright Page and a context object with the current story's id, title, and name. Executes within a test after a story is rendered. Useful for asserting things after the story is rendered, such as DOM and image snapshotting.

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // execute whatever you like, after the story renders
  },
};
export default config;

Note Although you have access to Playwright's Page object, in some of these hooks, we encourage you to test as much as possible within the story's play function.

Render lifecycle

To visualize the test lifecycle with these hooks, consider a simplified version of the test code automatically generated for each story in your Storybook:

// executed once, before the tests
await setup();

it('button--basic', async () => {
  // filled in with data for the current story
  const context = { id: 'button--basic', title: 'Button', name: 'Basic' };

  // playwright page https://playwright.dev/docs/pages
  await page.goto(STORYBOOK_URL);

  // pre-visit hook
  if (preVisit) await preVisit(page, context);

  // render the story and watch its play function (if applicable)
  await page.execute('render', context);

  // post-visit hook
  if (postVisit) await postVisit(page, context);
});

These hooks are very useful for a variety of use cases, which are described in the recipes section further below.

Apart from these hooks, there are additional properties you can set in .storybook/test-runner.js:

prepare

The test-runner has a default prepare function which gets the browser in the right environment before testing the stories. You can override this behavior, in case you might want to hack the behavior of the browser. For example, you might want to set a cookie, or add query parameters to the visiting URL, or do some authentication before reaching the Storybook URL. You can do that by overriding the prepare function.

The prepare function receives an object containing:

For reference, please use the default prepare function as a starting point.

Note If you override the default prepare behavior, even though this is powerful, you will be responsible for properly preparing the browser. Future changes to the default prepare function will not get included in your project, so you will have to keep an eye out for changes in upcoming releases.

getHttpHeaders

The test-runner makes a few fetch calls to check the status of a Storybook instance, and to get the index of the Storybook's stories. Additionally, it visits a page using Playwright. In all of these scenarios, it's possible, depending on where your Storybook is hosted, that you might need to set some HTTP headers. For example, if your Storybook is hosted behind a basic authentication, you might need to set the Authorization header. You can do so by passing a getHttpHeaders function to your test-runner config. That function receives the url of the fetch calls and page visits, and should return an object with the headers to be set.

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  getHttpHeaders: async (url) => {
    const token = url.includes('prod') ? 'XYZ' : 'ABC';
    return {
      Authorization: `Bearer ${token}`,
    };
  },
};
export default config;

tags (experimental)

The tags property contains three options: include | exclude | skip, each accepting an array of strings:

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  tags: {
    include: [], // string array, e.g. ['test-only']
    exclude: [], // string array, e.g. ['design', 'docs-only']
    skip: [], // string array, e.g. ['design']
  },
};
export default config;

tags are used for filtering your tests. Learn more here.

logLevel

When tests fail and there were browser logs during the rendering of a story, the test-runner provides the logs alongside the error message. The logLevel property defines what kind of logs should be displayed:

  • info (default): Shows console logs, warnings, and errors.
  • warn: Shows only warnings and errors.
  • error: Displays only error messages.
  • verbose: Includes all console outputs, including debug information and stack traces.
  • none: Suppresses all log output.
// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  logLevel: 'verbose',
};
export default config;

Utility functions

For more specific use cases, the test runner provides utility functions that could be useful to you.

getStoryContext

While running tests using the hooks, you might want to get information from a story, such as the parameters passed to it, or its args. The test runner now provides a getStoryContext utility function that fetches the story context for the current story:

Suppose your story looks like this:

// ./Button.stories.ts

export const Primary = {
  parameters: {
    theme: 'dark',
  },
};

You can access its context in a test hook like so:

// .storybook/test-runner.ts
import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // Get entire context of a story, including parameters, args, argTypes, etc.
    const storyContext = await getStoryContext(page, context);
    if (storyContext.parameters.theme === 'dark') {
      // do something
    } else {
      // do something else
    }
  },
};
export default config;

It's useful for skipping or enhancing use cases like image snapshot testing, accessibility testing and more.

waitForPageReady

The waitForPageReady utility is useful when you're executing image snapshot testing with the test-runner. It encapsulates a few assertions to make sure the browser has finished downloading assets.

// .storybook/test-runner.ts
import { TestRunnerConfig, waitForPageReady } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // use the test-runner utility to wait for fonts to load, etc.
    await waitForPageReady(page);

    // by now, we know that the page is fully loaded
  },
};
export default config;

StorybookTestRunner user agent

The test-runner adds a StorybookTestRunner entry to the browser's user agent. You can use it to determine if a story is rendering in the context of the test runner. This might be useful if you want to disable certain features in your stories when running in the test runner, though it's likely an edge case.

// At the render level, useful for dynamically rendering something based on the test-runner
export const MyStory = {
  render: () => {
    const isTestRunner = window.navigator.userAgent.match(/StorybookTestRunner/);
    return (
      <div>
        <p>Is this story running in the test runner?</p>
        <p>{isTestRunner ? 'Yes' : 'No'}</p>
      </div>
    );
  },
};

Given that this check is happening in the browser, it is only applicable in the following scenarios:

  • inside of a render/template function of a story
  • inside of a play function
  • inside of preview.js
  • inside any other code that is executed in the browser

Recipes

Below you will find recipes that use both the hooks and the utility functions to achieve different things with the test-runner.

Preconfiguring viewport size

You can use Playwright's Page viewport utility to programatically change the viewport size of your test. If you use @storybook/addon-viewports, you can reuse its parameters and make sure that the tests match in configuration.

// .storybook/test-runner.ts
import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner';
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';

const DEFAULT_VIEWPORT_SIZE = { width: 1280, height: 720 };

const config: TestRunnerConfig = {
  async preVisit(page, story) {
    const context = await getStoryContext(page, story);
    const viewportName = context.parameters?.viewport?.defaultViewport;
    const viewportParameter = MINIMAL_VIEWPORTS[viewportName];

    if (viewportParameter) {
      const viewportSize = Object.entries(viewportParameter.styles).reduce(
        (acc, [screen, size]) => ({
          ...acc,
          // make sure your viewport config in Storybook only uses numbers, not percentages
          [screen]: parseInt(size),
        }),
        {}
      );

      page.setViewportSize(viewportSize);
    } else {
      page.setViewportSize(DEFAULT_VIEWPORT_SIZE);
    }
  },
};
export default config;

Accessibility testing

You can install axe-playwright and use it in tandem with the test-runner to test the accessibility of your components. If you use @storybook/addon-a11y, you can reuse its parameters and make sure that the tests match in configuration, both in the accessibility addon panel and the test-runner.

// .storybook/test-runner.ts
import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner';
import { injectAxe, checkA11y, configureAxe } from 'axe-playwright';

const config: TestRunnerConfig = {
  async preVisit(page, context) {
    // Inject Axe utilities in the page before the story renders
    await injectAxe(page);
  },
  async postVisit(page, context) {
    // Get entire context of a story, including parameters, args, argTypes, etc.
    const storyContext = await getStoryContext(page, context);

    // Do not test a11y for stories that disable a11y
    if (storyContext.parameters?.a11y?.disable) {
      return;
    }

    // Apply story-level a11y rules
    await configureAxe(page, {
      rules: storyContext.parameters?.a11y?.config?.rules,
    });

    // in Storybook 6.x, the selector is #root
    await checkA11y(page, '#storybook-root', {
      detailedReport: true,
      detailedReportOptions: {
        html: true,
      },
      // pass axe options defined in @storybook/addon-a11y
      axeOptions: storyContext.parameters?.a11y?.options,
    });
  },
};
export default config;

DOM snapshot (HTML)

You can use Playwright's built in APIs for DOM snapshot testing:

// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';

const config: TestRunnerConfig = {
  async postVisit(page, context) {
    // the #storybook-root element wraps the story. In Storybook 6.x, the selector is #root
    const elementHandler = await page.$('#storybook-root');
    const innerHTML = await elementHandler.innerHTML();
    expect(innerHTML).toMatchSnapshot();
  },
};
export default config;

When running with --stories-json, tests get generated in a temporary folder and snapshots get stored alongside. You will need to --eject and configure a custom snapshotResolver to store them elsewhere, e.g. in your working directory:

// ./test-runner-jest.config.js
const { getJestConfig } = require('@storybook/test-runner');

const testRunnerConfig = getJestConfig();

/**
 * @type {import('@jest/types').Config.InitialOptions}
 */
module.exports = {
  // The default Jest configuration comes from @storybook/test-runner
  ...testRunnerConfig,
  snapshotResolver: './snapshot-resolver.js',
};
// ./snapshot-resolver.js
const path = require('path');

// πŸ‘‰ process.env.TEST_ROOT will only be available in --index-json or --stories-json mode.
// if you run this code without these flags, you will have to override it the test root, else it will break.
// e.g. process.env.TEST_ROOT = process.cwd()

module.exports = {
  resolveSnapshotPath: (testPath, snapshotExtension) =>
    path.join(process.cwd(), '__snapshots__', path.basename(testPath) + snapshotExtension),
  resolveTestPath: (snapshotFilePath, snapshotExtension) =>
    path.join(process.env.TEST_ROOT, path.basename(snapshotFilePath, snapshotExtension)),
  testPathForConsistencyCheck: path.join(process.env.TEST_ROOT, 'example.test.js'),
};

Image snapshot

Here's a slightly different recipe for image snapshot testing:

// .storybook/test-runner.ts
import { TestRunnerConfig, waitForPageReady } from '@storybook/test-runner';
import { toMatchImageSnapshot } from 'jest-image-snapshot';

const customSnapshotsDir = `${process.cwd()}/__snapshots__`;

const config: TestRunnerConfig = {
  setup() {
    expect.extend({ toMatchImageSnapshot });
  },
  async postVisit(page, context) {
    // use the test-runner utility to wait for fonts to load, etc.
    await waitForPageReady(page);

    // If you want to take screenshot of multiple browsers, use
    // page.context().browser().browserType().name() to get the browser name to prefix the file name
    const image = await page.screenshot();
    expect(image).toMatchImageSnapshot({
      customSnapshotsDir,
      customSnapshotIdentifier: context.id,
    });
  },
};
export default config;

Troubleshooting

Yarn PnP (Plug n' Play) support

The Storybook test-runner relies on a library called jest-playwright-preset, of which does not seem to support PnP. As a result, the test-runner won't work out of the box with PnP, and you might have the following error:

PlaywrightError: jest-playwright-preset: Cannot find playwright package to use chromium

If that is the case, there are two potential solutions:

  1. Install playwright as a direct dependency. You might need to run yarn playwright install after that, so you install Playwright's browser binaries.
  2. Switch your package manager's linker mode to node-modules.

React Native support

The test-runner is web based and therefore won't work with @storybook/react-native directly. However, if you use the React Native Web Storybook Addon, you can run the test-runner against the web-based Storybook generated with that addon. In that case, things would work the same way.

The error output in the CLI is too short

By default, the test runner truncates error outputs at 1000 characters, and you can check the full output directly in Storybook, in the browser. If you do want to change that limit, however, you can do so by setting the DEBUG_PRINT_LIMIT environment variable to a number of your choosing, for example, DEBUG_PRINT_LIMIT=5000 yarn test-storybook.

The test runner seems flaky and keeps timing out

If your tests are timing out with Timeout - Async callback was not invoked within the 15000 ms timeout specified by jest.setTimeout, it might be that playwright couldn't handle to test the amount of stories you have in your project. Maybe you have a large amount of stories or your CI has a really low RAM configuration.

In either way, to fix it you should limit the amount of workers that run in parallel by passing the --maxWorkers option to your command:

{
  "test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && yarn test-storybook --maxWorkers=2\""
}

The test runner reports "No tests found" running on a Windows CI

There is currently a bug in Jest which means tests cannot be on a separate drive than the project. To work around this you will need to set the TEMP environment variable to a temporary folder on the same drive as your project. Here's what that would look like on GitHub Actions:

env:
  # Workaround for https://github.com/facebook/jest/issues/8536
  TEMP: ${{ runner.temp }}

Adding the test runner to other CI environments

As the test runner is based on playwright, depending on your CI setup you might need to use specific docker images or other configuration. In that case, you can refer to the Playwright CI docs for more information.

Merging test coverage results in wrong coverage

After merging test coverage reports coming from the test runner with reports from other tools (e.g. Jest), if the end result is not what you expected. Here's why:

The test runner uses babel as coverage provider, which behaves in a certain way when evaluating code coverage. If your other reports happen to use a different coverage provider than babel, such as v8, they will evaluate the coverage differently. Once merged, the results will likely be wrong.

Example: in v8, import and export lines are counted as coverable pieces of code, however in babel, they are not. This impacts the percentage of coverage calculation.

While the test runner does not provide v8 as an option for coverage provider, it is recommended that you set your application's Jest config to use coverageProvider: 'babel' if you can, so that the reports line up as expected and get merged correctly.

For more context, here's some explanation why v8 is not a 1:1 replacement for Babel/Istanbul coverage.

Future work

Future plans involve adding support for the following features:

  • πŸ“„ Run addon reports
  • βš™οΈ Spawning Storybook via the test runner in a single command

Contributing

We welcome contributions to the test runner!

Branch structure

  • next - the next version on npm, and the development branch where most work occurs
  • prerelease - the prerelease version on npm, where eventual changes to main get tested
  • main - the latest version on npm and the stable version that most users use

Release process

  1. All PRs should target the next branch, which depends on the next version of Storybook.
  2. When merged, a new version of this package will be released on the next NPM tag.
  3. If the change contains a bugfix that needs to be patched back to the stable version, please note that in PR description.
  4. PRs labeled pick will get cherry-picked back to the prerelease branch and will generate a release on the prerelease npm tag.
  5. Once validated, prerelease PRs will get merged back to the main branch, which will generate a release on the latest npm tag.

test-runner's People

Contributors

andykenward avatar bodograumann avatar bryanjtc avatar ferdinandhummel-gph avatar fokkezb avatar ianvs avatar italoteix avatar jaknas avatar jonniebigodes avatar joshwooding avatar jreinhold avatar junkisai avatar kaelig avatar kasperpeulen avatar kemuridama avatar legobeat avatar mh4gf avatar michaelhatherly avatar ndelangen avatar niznikr avatar shilman avatar stramel avatar tmeasday avatar unshame avatar valentinpalkovic avatar vanessayuenn avatar vezaynk avatar work933k avatar yannbf avatar zyulyaev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

test-runner's Issues

[Bug]ReferenceError: __test is not defined

I seem to get this error seemingly at random when running the tests.
Following the stack trace just takes me to the very bottom of my story file.

 FAIL   browser: chromium  src/Tag.stories.tsx (26.779 s)
  ● Base Components/Tag β€Ί Variants β€Ί smoke-test

    page.evaluate: ReferenceError: __test is not defined

      at eval (eval at evaluate (:3:2389), <anonymous>:4:17)
      at t.default.evaluate (<anonymous>:3:2412)
      at t.default.<anonymous> (<anonymous>:1:44)
      at testFn (src/Tag.stories.tsx:84:37)
      at Object.<anonymous> (src/Tag.stories.tsx:99:17)

Provide a method to retrieve parameters in the API hooks

Currently there is no stylized way to retrieve story parameters in API hooks.

This would be useful for reusing:

  • viewports configurations
  • a11y parameters
  • etc.

This could be provided automatically by the test-runner as part of the context argument.

  async preRender(page, context) {
    const { parameters } = context;
  },

Or maybe on=demand:

  async preRender(page, context) {
    const parameters = await getParameters(page, context);
    // OR
    const parameters = await context.getParameters(page);
  },

The benefit of on-demand is that the user would only "pay" the performance cost of retrieving parameters when they are actually used.

[Feature] Allow setting test description

Describe the Feature

Sometimes it is nice to give some extra context around what the test is doing, and I find that the story name is not always an ideal place for this. I've taken to just adding comments at the top of my play function, but these don't show up in the test result output of course. It might be nice to have a way to customize what is shown there instead of always just smoke-test or play-test.

Additional context

I'm not really sure how'd you'd accomplish this, short of adding another property onto CSF objects, which does not feel ideal. My only other idea would be to allow a return value from play functions, which you could use to pass before/after hooks, and maybe also a description.

test-storybook fails for .stories.play files

Describe the bug

Running test-storybook fails for automated stories (.stories.play files).

Steps to reproduce

Following the steps here.

Install the test runner:

yarn add --dev @storybook/test-runner

Ensure Jest is installed.

Add a package script to run the tests:

"scripts": { "test-storybook": "test-storybook", }

Run storybook (test runner runs against a running instance, default on localhost:6006):

yarn storybook

Run the tests:

'yarn test-storybook`

Expected behaviour

Tests pass

Screenshots and/or logs

UnderConstruction.stories.tsx

import { ComponentStory, ComponentMeta } from '@storybook/react';
import { UnderConstruction } from './UnderConstruction';

export default {
  title: 'GW-WEB-UI/UnderConstruction/Manual',
  component: UnderConstruction,
  argTypes: {
    text: { name: 'text' },
    buttonText: { name: 'buttonText' },
  },
  parameters: {
    backgrounds: {
      values: [
        { name: 'red', value: '#f00' },
        { name: 'green', value: '#0f0' },
        { name: 'blue', value: '#00f' },
        { name: 'hot pink', value: '#FF69B4' },
      ],
    },
    decorators: [
      (Story: any) => (
        <div className="p-8 flex items-center justify-center bg-white">
          <div className="w-full max-w-xs mx-auto">
            <Story />
          </div>
        </div>
      ),
    ],
  },
} as ComponentMeta<typeof UnderConstruction>;

const template: ComponentStory<typeof UnderConstruction> = (args) => <UnderConstruction {...args} />;

export const Primary = template.bind({});

Primary.args = {
  text: 'Under Construction',
  buttonText: 'Click me to change colour!',
};

export const Secondary = template.bind({});

Secondary.args = {
  ...Primary.args,
  buttonText: 'See what happens when you click me!',
};

UnderConstruction.stories.play.tsx

import { ComponentStory, ComponentMeta } from '@storybook/react';
import { within, userEvent } from '@storybook/testing-library';
import { UnderConstruction } from './UnderConstruction';

export default {
  title: 'GW-WEB-UI/UnderConstruction/Automated',
  component: UnderConstruction,
  argTypes: {
    text: { name: 'text' },
    buttonText: { name: 'buttonText' },
  },
} as ComponentMeta<typeof UnderConstruction>;

const template: ComponentStory<typeof UnderConstruction> = (args) => <UnderConstruction {...args} />;

export const Primary = template.bind({});

Primary.args = {
  text: 'Under Construction',
  buttonText: 'Click me to change colour!',
};

Primary.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  await userEvent.type(canvas.getByTestId('input'), 'Hello');
  await userEvent.type(canvas.getByTestId('input'), ' Slowpoke', {
    delay: 500,
  });
  await userEvent.click(canvas.getByTestId('setColorButton'));
};

Error output:

Screenshot 2022-04-27 at 16 13 37

main.js (storybook configuration file)

module.exports = {
  stories: ['../src/**/**/*.stories.@(js|jsx|ts|tsx)', '../src/**/**/*.stories.play.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    // required for tailwind css
    {
      name: '@storybook/addon-postcss',
      options: {
        postcssLoaderOptions: {
          implementation: require('postcss'),
        },
      },
    },
    '@storybook/addon-viewport',
  ],
  features: {
    interactionsDebugger: true,
  },
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-vite',
  },
  //ISSUE: https://github.com/storybookjs/storybook/issues/17852
  features: { storyStoreV7: true },
};

Environment

MacOS Big Sur
Node 16.10.0
Storybook 6.4.2
Jest
Typescript

How do we use stories.json on Chromatic?

Question

How to use chromatic stories.json in GitHub Actions with test-runner?

Background

Our storybooks are hosted on chromatic, then we're aiming to execute interaction test in GitHub Actions by using Chromatic hosting stories.json. However, we cannot pass through authentication by this test-runner. I've tried like this

$ REFERENCE_URL=<chromatic-permalink> TARGET_URL=<chromatic-permalink> yarn test-storybook 

Cited from https://github.com/storybookjs/test-runner#2-running-against-locally-built-storybooks-in-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.

[Docs] Missing jest dependency

Describe the bug

Jest should either be a dependency or a peer dependency

EDIT: It is a peer dependency, but we should document in README

Steps to reproduce the behavior

Add @storybook/test-runner to a project that doesn't have jest already installed e.g. @storybook/design-system

[Feature Request] Keep browser loaded between tests

Describe the Feature request

Currently, it seems that a new browser window is being opened for each test. Unfortunately, that causes a long loading time in development for vite projects, which do not bundle modules in development, which can result in thousands of requests at page load (with the promised payback of faster hot module reload times). Currently, I'm seeing load times of 20-30 seconds per test. Production is much faster, because vite does bundle production builds.

Ideally, it would be great if a single browser window could remain open, and navigation to different stories could happen via history.replaceState or something, rather than full new page loads. I'm not sure how feasible that is, honestly, but it could help speed things up, maybe.

Additional context

FWIW, web-test-runner uses websockets in its interactions with playwright, which means it only needs to open the browser once, and each test is injected via ws. Not sure if that would work here, but it's an idea.

Set up argument parsing in CLI

We currently support all Jest CLI arguments and pass them directly to Jest.

Per @ghengeveld 's suggestion, let's prune these down to the set that we want to support. That way if users want other options, they can make feature requests and we can add them as we see fit.

Being more disciplined about this helps us understand which features users want, and also allows us to switch off Jest in the future should we see fit.

Incompatibility with Jest 28: `Got error running globalSetup`

Describe the bug

Starting npm run test-storybook fails with an TypeError

TypeError: Jest: Got error running globalSetup - /Users/ingo.fahrentholz/Developer/delme/storybook-testing/node_modules/@storybook/test-runner/playwright/global-setup.js, reason: Class extends value #<Object> is not a constructor or null
    at Object.getPlaywrightEnv (/Users/ingo.fahrentholz/Developer/delme/storybook-testing/node_modules/jest-playwright-preset/lib/PlaywrightEnvironment.js:59:48)
    at Object.<anonymous> (/Users/ingo.fahrentholz/Developer/delme/storybook-testing/node_modules/jest-playwright-preset/lib/PlaywrightEnvironment.js:242:27)
    at Module._compile (node:internal/modules/cjs/loader:1103:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1157:10)
    at Object.newLoader (/Users/ingo.fahrentholz/Developer/delme/storybook-testing/node_modules/pirates/lib/index.js:141:7)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/Users/ingo.fahrentholz/Developer/delme/storybook-testing/node_modules/jest-playwright-preset/index.js:1:18)
    at Module._compile (node:internal/modules/cjs/loader:1103:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1157:10)
    at Object.newLoader (/Users/ingo.fahrentholz/Developer/delme/storybook-testing/node_modules/pirates/lib/index.js:141:7)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)

Steps to reproduce the behavior

start storybook
start test-storybook

Expected behavior

test-runner should run the tests defined as play functions inside the story.

Environment

  • OS: macOS 12.3.1
  • Node.js version: 16.14.2
  • NPM version: 8.5.0

Repository

https://github.com/ifahrentholz/storybook-test-runner-bug

[Feature] Standalone stories.json mode

Currently, the test runner works by transforming CSF files into Jest tests. This is great because you can edit your stories & re-run your tests interactively.

However, sometimes all you have is a link to a publicly hosted storybook. If we had a --stories-json mode that could transform stories.json into a set of tests, then you could run tests against any Storybook with zero configuration.

jest-dom methods availability

Hi guys! Thanks for your work.
Can you help me with a receipt for jest-dom methods availability?

test-runner-jest.config.js:

import '@testing-library/jest-dom';

module.exports = {
  setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],
};

story.js:

import { expect } from '@storybook/jest';

...

// Inside `play` method:
expect(canvas.getByTestId('my-element-1')).toBeNull();
expect(canvas.getByTestId('my-element-2')).toBeVisible();

toBeNull β€” is worked;
toBeVisible β€” Is not worked: is not a function

I want to get jest-dom methods for expect, but how to do it right, can you help me? Sorry for the interruption.

Better negotiation against running storybook

  • Improve error message when Storybook is not running (e.g. mention about TARGET_URL for different ports)
  • If people run against a deployed Storybook (e.g. we can make a regex to check if URL is localhost or IP address), provide a prompt (that is skipped with β€”yes) to run stories-json mode to get things synchronised

[Feature] Allow preventing smoke-tests on particular stories

Describe the Feature

I have a Themed story in many of my stories files which simply renders other stories together in a "light mode" and "dark mode". If I'm testing each of those other stories, then I would like to save the time and prevent my Themed stories from being smoke-tested. As far as I know, there's no way to do this currently.

One approach might be to add a parameter, like

const Themed = {
  ...myCSF,
  parameters: {
    testing: {
      disable: true,
    },
  },
}

If this approach was taken, it might also be a way to set the it description of the test (see #71), even though it's a little clunky. In addition, maybe it would be a cool way to add setup/teardown hooks for a particular story. Even though I haven't found that I need those so far, but I think that's because each test is a fresh page load. Which is great for isolation, but definitely slows things down. And the individual stories themselves are not new page loads, so there could need to be setup/teardown between them. But that's probably a separate issue.

[Bug] Running the test runner against a Storybook server using self-signed SSL certs fails

Describe the bug

When using SSL with a self-signed certificate to run the development server, the test runner (with at least the Chromium browser) will reject the SSL connection, probably because it fails to validate the CA signing the certificate (that's my educated guess).

The test runner needs an option to either inject the root CA into the running instance of the browser, or disable verifying SSL certificates when connecting through HTTPS.

Steps to reproduce the behavior

To test this, generate a CA cert and a corresponding domain certificate. For convenience, I've added two bash scripts to generate the CA and domain wildcard self-signed certs to enable SSL. Not that you will need to add the root.pem certificate to your trusted root CA store in order to be able to use certificates generated by this CA.

generate-ca

#!/bin/bash

set -eu

ROOT_DIR=$(dirname $0)
BUILD_DIR="$ROOT_DIR/build"

mkdir -p "$BUILD_DIR"

openssl genrsa -out "$BUILD_DIR/root.key" 2048
openssl req -x509 -new -nodes -key "$BUILD_DIR/root.key" -sha256 -days 365 -out "$BUILD_DIR/root.pem"
openssl x509 -text -noout -in "$BUILD_DIR/root.pem"

generate-domain-cert

#!/bin/bash

set -e

ROOT_DIR=$(dirname $0)
BUILD_DIR="$ROOT_DIR/build"

ROOT_CERT="$BUILD_DIR/root.pem"
ROOT_KEY="$BUILD_DIR/root.key"

if [ ! -f "$ROOT_CERT" ]
then
  echo "Root certificate not found! Use generate-ca to generate a root CA certificate"
  exit 1
elif [ ! -f "$ROOT_KEY" ]
then
  echo "Root certificate key not found! Use generate-ca to generate a root CA certificate"
  exit 1
fi

if [ "$1" = "" ]
then
  echo "You must provide a FQDN name, e.g. ./generate-domain-name domain.name"
  exit 2
fi

DOMAIN_NAME="$1"
DOMAIN_DIR="$BUILD_DIR/$DOMAIN_NAME"

mkdir -p "$DOMAIN_DIR"

DOMAIN_KEY="$DOMAIN_DIR/wildcard.key"
DOMAIN_CERT="$DOMAIN_DIR/wildcard.crt"
DOMAIN_CSR="$DOMAIN_DIR/wildcard.csr"
DOMAIN_CONFIG_NAME="$DOMAIN_DIR/config.cnf"

openssl genrsa -out "$DOMAIN_KEY" 2048

cat <<EOF > "$DOMAIN_CONFIG_NAME"
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = $DOMAIN_NAME
[v3_req]
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 127.0.0.1
DNS.1 = localhost
DNS.2 = *.localhost
DNS.3 = $DOMAIN_NAME
DNS.4 = *.$DOMAIN_NAME
EOF

openssl req -new -out "$DOMAIN_CSR" -key "$DOMAIN_KEY" -config "$DOMAIN_CONFIG_NAME"

openssl x509 -req -in "$DOMAIN_CSR" \
-CA "$ROOT_CERT" \
-CAkey "$ROOT_KEY" \
-CAcreateserial \
-out "$DOMAIN_CERT" \
-days 365 \
-sha256 \
-extensions v3_req \
-extfile "$DOMAIN_CONFIG_NAME"

openssl verify -CAfile "$ROOT_CERT" "$DOMAIN_CERT"

openssl x509 -text -noout -in "$DOMAIN_CERT"

Run the scripts:

./generate-ca
./generate-domain-cert mydomain.com

Make sure that mydomain.com resolves to the IP of the machine running the Storybook server.

Then run the Storybook dev server together with the test runner:

concurrently -k -s first \
  "start-storybook -p 6006 -h mydomain.com --https --ssl-ca ./build/root.pem --ssl-cert ./build/mydomain.com/wildcard.crt --ssl-key ./build/mydomain.com/wildcard.key" \
  "wait-on tcp:6006 && STORYBOOK_URL=https://mydomain.com:6006 yarn test-storybook"

Verify that the Storybook is reachable by issuing CURL or by opening the URL in a browser (this verifies your system trusts the root CA cert you've generated):

curl https://mydomain:6006/

The test runner fails when trying to run tests:

$ yarn run test-storybook
yarn run v1.22.17
$ test-storybook --url "${STORYBOOK_URL:-https://mydomain.com:6006}"
[test-storybook] It seems that your Storybook instance is not running at: https://mydomain.com:6006/. Are you sure it's running?

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

More info at https://github.com/storybookjs/test-runner#getting-started
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Expected behavior

The CURL should respond with HTML (as a sanity check) and the test runner should be able to run the tests.

Environment

  • OS: Debian
  • Node.js version: v16.13.0
  • NPM version: 8.1.3
  • Browser (if applicable): Chromium

[Bug] Tests always run in the default viewport

Describe the bug

The test runner runs tests full screen inside the default playwright Chrome viewport. It does not resize the viewport according to the viewport parameter of the story.

Steps to reproduce the behavior

I have a component where a dropdown is hidden on large screens using a media query. I configured the story to use a mobile2 viewport by default and wrote a play function to test the visibility of the element:

export const Mobile = Template.bind({})
Mobile.parameters = {
  viewport: {
    defaultViewport: 'mobile2',
  },
}
Mobile.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement)
  const toggleButton = await canvas.findByTestId('dropdownToggle')
  await waitFor(() => expect(toggleButton).toBeVisible())
}

When I run storybook locally and use the interactions addon, the test runs fine, but when I run the test runner, it fails.

I debugged the test runner using this custom test-runner-jest.config.js, --maxWorkers 1 and by adding sleep timeouts after every line. I can see that the playwright Chrome window has the wrong size and that it loads the story iframe in fullscreen.

const { getJestConfig } = require('@storybook/test-runner');

module.exports = {
  ...getJestConfig(),
  testEnvironmentOptions: {
    'jest-playwright': {
      launchOptions: {
        headless: false,
      }
    },
  },
}

Environment

  • Storybook 6.4.20
  • @storybook/test-runner 0.0.4
  • @storybook/testing-library 0.0.9

Additional context

As a workaround, I wrote a prerender hook in .storybook/test-runner.js that resizes the playwright page if the test has Mobile in its name. A solution that does not rely on magic strings would be better though.

module.exports = {
  preRender(page, story) {
    if (story.name.includes('Mobile')) {
      page.setViewportSize({ width: 896, height: 414 })
    }
  },
}

CI setup

  • Chromatic
  • Mealdrop
  • Design system

[Feature] Use storyName in test name

Describe the bug

I'm not sure to call this a bug or a feature, but I find myself wishing that the story name displayed in the test output matched storyName rather than the name of the exported CSF constant. This would make it easier to find the test in the storybook sidebar, in cases where there's a custom storyName that's being used.

Steps to reproduce the behavior

  1. Create a CSF story with a custom storyName that does not match the story's exported name.
  2. Run test-runner
  3. Notice that the test name displayed does not match the storyName

Additional context

This is a pretty minor request, but wanted to document it since I did find it tripping me up once or twice.

`test-storybook` is displaying `PASS` when the story has a TypeError

Describe the bug

test-storybook is displaying PASS when the story has a TypeError(s).

Steps to reproduce the behavior

  1. create a story that has type errors, e.g. BlogPost.stories.tsx
// BlogPost.stories.tsx
import BlogPost from './BlogPost'

export const generated = () => {
  return <BlogPost />
}

export default { title: 'Components/BlogPost' }
// BlogPost.tsx
interface Props {
  post: { id: number, title: string, body: string, createdAt: string }
}


const BlogPost = ({ post }: Props) => {
  return (

    <article>
      <header className="mt-4">
        <p className="text-sm">
          {new Intl.DateTimeFormat('en-US', {  year: 'numeric', month: 'long', day: 'numeric' }).format(new Date(post.createdAt))}
        </p>
        <h2 className="text-xl mt-2 font-semibold">
          <div className="hover:text-blue-600">{post.title}</div>
        </h2>
      </header>
      <div className="mt-2 mb-4 text-gray-900 font-light">{post.body}</div>
    </article>

  );
}

export default BlogPost
  1. run test-storybook

Expected behavior

test-storybook fails for those stories which have a TypeError(s)

Screenshots and/or logs

image

image

$ yarn workspace web test-storybook
yarn workspace v1.23.0-20211220.1904
yarn run v1.23.0-20211220.1904
$ test-storybook
page loaded in 1847ms.
page loaded in 1943ms.
page loaded in 2047ms.
page loaded in 1919ms.
page loaded in 2193ms.
page loaded in 2281ms.
page loaded in 2230ms.
page loaded in 2289ms.
 PASS   browser: chromium  src/pages/ContactPage/ContactPage.stories.tsx
 PASS   browser: chromium  src/pages/AboutPage/AboutPage.stories.tsx (5.231 s)
 PASS   browser: chromium  src/pages/BlogPostPage/BlogPostPage.stories.tsx (5.235 s)
 PASS   browser: chromium  src/components/BlogPost/BlogPost.stories.tsx (5.101 s)
 PASS   browser: chromium  src/layouts/BlogLayout/BlogLayout.stories.tsx (5.221 s)
 PASS   browser: chromium  src/pages/HomePage/HomePage.stories.tsx (5.186 s)
 PASS   browser: chromium  src/components/BlogPostCell/BlogPostCell.stories.tsx (5.441 s)
 PASS   browser: chromium  src/components/BlogPostsCell/BlogPostsCell.stories.tsx (5.061 s)

Test Suites: 8 passed, 8 total
Tests:       14 passed, 14 total
Snapshots:   0 total
Time:        8.308 s
Ran all test suites.
Done in 10.74s.
Done in 11.12s.

[Bug] test-runner-jest.config.js should refer to absolute paths

Describe the bug

Published test-runner-jest.config.js should refer to absolute paths:

  globalSetup: '@storybook/test-runner/playwright/global-setup.js',
  globalTeardown: '@storybook/test-runner/playwright/global-teardown.js',
  testEnvironment: '@storybook/test-runner/playwright/custom-environment.js',

This makes it harder to develop, but easier for users to copy the config file and edit themselves. Plus a fresh install fails for me with:

MMBP15:design-system shilman$ yarn test-storybook
yarn run v1.22.17
$ /Users/shilman/projects/storybookjs/design-system/node_modules/.bin/test-storybook
● Validation Error:

  Test environment ./playwright/custom-environment.js cannot be found. Make sure the testEnvironment configuration option points to an existing node module.

  Configuration Documentation:
  https://jestjs.io/docs/configuration

error Command failed with exit code 1.

Steps to reproduce the behavior

Add @storybook/test-runner to a project that doesn't have jest already installed e.g. @storybook/design-system

TypeError: Jest: a transform must export a `process` function

πŸ‘‹ I am trying to add test-runner to an existing project that already has Storybook and Jest running, when following the Getting started instructions, I end up with the following error for each of my existing stories:

 FAIL   browser: chromium  spec/storybook/pages/request/index.stories.tsx
  ● Test suite failed to run

    TypeError: Jest: a transform must export a `process` function.

      at ScriptTransformer._getTransformer (node_modules/@jest/transform/build/ScriptTransformer.js:360:13)
      at ScriptTransformer.transformSource (node_modules/@jest/transform/build/ScriptTransformer.js:427:28)
      at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:569:40)
      at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:607:25)
      at interopRequireDefault (node_modules/@storybook/core-common/dist/cjs/utils/interpret-require.js:64:16)

Steps to reproduce

  1. run yarn storybook in terminal, wait for it to finish
  2. run yarn test-storybook in another terminal

Current packages

  • My major versions of ts-jest and jest match.
  • I am using Webpack 4

Any thoughts on this?

Respect main.js glob in selecting story files

Describe the bug

Currently the test-runner selects *.stories.[tj]sx? but this information is already more precisely specified in .storybook/main.js.

We should use main.js to select the stories:

  • Regular globs
  • Strip out MDX
  • Handle CSF3-style specifiers

[Bug] Sync issue in failures from one test affecting another test

Describe the bug

This seems not to be an actual bug in the test-runner, but rather in Storybook. Here's an example of a diagram that shows the sequence of steps in the test-runner:

image

Steps to reproduce the behavior

  1. Change the Demo story in Button.stories.js from:
export const Demo = (args) => (
  <button type="button" onClick={() => args.onSubmit('clicked')}>
    Click
  </button>
);

to:

export const Demo = (args) => (
  <button type="button" onClick={() => { throw new Error('boom') }}>
    Click
  </button>
);
  1. Run yarn test-storybook

The boom error should not leak into the FindBy story, which actually does not even use the same component as Demo:
image

[Bug] Unable to run with custom-config in an esm-project

Describe the bug

If I eject the config in an esm-project, the filename test-runner-jest.config.js is interpreted as esm-module and jest will fail to run. As the filename is hardcoded, I'm unable to rename the file to test-runner-jest.config.cjs.

Two possible solutions:

  1. make the filename of the config adjustable via parameter
  2. test for the existance of test-runner-jest.config.cjs in the cli

[Bug] Does not find svelte stories

Describe the bug

I've just added @storybook/test-runner to the storybook-builder-vite, and found that tests are not running correctly on our svelte example project. I get the following message:

YN0000: No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In /home/runner/work/storybook-builder-vite/storybook-builder-vite/examples/svelte
   22 files checked.
   testMatch: /home/runner/work/storybook-builder-vite/storybook-builder-vite/examples/svelte/stories/**/*.stories.mdx, /home/runner/work/storybook-builder-vite/storybook-builder-vite/examples/svelte/stories/**/*.stories.@(js|jsx|ts|tsx|svelte) - 0 matches
   testPathIgnorePatterns: /node_modules/ - 22 matches
   testRegex:  - 0 matches
Pattern:  - 0 matches
ERROR: "test" exited with 1.

This can be seen in a CI run here: https://github.com/eirslett/storybook-builder-vite/runs/5538433882?check_suite_focus=true#step:7:47

Steps to reproduce the behavior

  1. Clone the storybook-builder-vite repo
  2. git checkout storybook-test-ci
  3. yarn install
  4. cd examples/svelte
  5. yarn build-storybook
  6. yarn test-ci

Expected behavior

I expect that the stories are found, and tests are run correctly.

Additional context

We are using svelte component stories, maybe the test-runner does not support them? I didn't see any notes or issues about it yet, though.

[Bug] Storybook fail on start after adding '@storybook/addon-interactions' to .storybook/main.js

Describe the bug

After installing @storybook/test-runner and following the Getting Started documentation, starting storybook fails to start and throws the following error:

ModuleNotFoundError: Module not found: Error: Can't resolve './.storybook/@storybook' in </home/....>

Steps to reproduce the behavior

  1. Install storybook/react 6.4.19 with addon-essentuals, addon-actions, addon-interaction, add-links
  2. Install @storybook/jest 0.0.9 and @storybook/testing-library 0.0.9
  3. install @storybook/test-runner 0.0.4
  4. Follow the getting started and add this to the ./storybook/main.js:
  stories: ['@storybook/addon-interactions'],
  features: {
    interactionsDebugger: true,
  },
};
  1. run storybook yarn storybook (in my case npm run storybook)
  2. Storybook fail to start with error:
ModuleNotFoundError: Module not found: Error: Can't resolve './.storybook/@storybook' in </home/....>

Expected behavior

Storybook should be able to start without error.

Screenshots and/or logs

npm run storybook logs:

ModuleNotFoundError: Module not found: Error: Can't resolve './.storybook/@storybook' in '~/web-app'
    at ~/web-app/node_modules/webpack/lib/Compilation.js:2014:28
    at ~/web-app/node_modules/webpack/lib/ContextModuleFactory.js:210:15
    at ~/web-app/node_modules/neo-async/async.js:2830:7
    at ~/web-app/node_modules/neo-async/async.js:6877:13
    at ~/web-app/node_modules/webpack/lib/ContextModuleFactory.js:180:26
    at finishWithoutResolve (~/web-app/node_modules/webpack/node_modules/enhanced-resolve/lib/Resolver.js:307:11)
    at ~/web-app/node_modules/webpack/node_modules/enhanced-resolve/lib/Resolver.js:381:15
    at ~/web-app/node_modules/webpack/node_modules/enhanced-resolve/lib/Resolver.js:430:5
    at eval (eval at create (~/web-app/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:16:1)
    at ~/web-app/node_modules/webpack/node_modules/enhanced-resolve/lib/Resolver.js:430:5
    at eval (eval at create (~/web-app/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:27:1)
    at ~/web-app/node_modules/webpack/node_modules/enhanced-resolve/lib/DescriptionFilePlugin.js:87:43
    at ~/web-app/node_modules/webpack/node_modules/enhanced-resolve/lib/Resolver.js:430:5
    at eval (eval at create (~/web-app/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:16:1)
    at ~/web-app/node_modules/webpack/node_modules/enhanced-resolve/lib/forEachBail.js:16:12
    at ~/web-app/node_modules/webpack/node_modules/enhanced-resolve/lib/AliasPlugin.js:103:14
resolve './.storybook/@storybook' in '~/web-app'
  using description file: ~/web-app/package.json (relative path: .)
    Field 'browser' doesn't contain a valid alias configuration
    using description file: ~/web-app/package.json (relative path: ./.storybook/@storybook)
      ~/web-app/.storybook/@storybook doesn't exist

Dev Dependencies:

"@babel/core": "^7.17.5",
"@storybook/addon-actions": "^6.4.19",
"@storybook/addon-essentials": "^6.4.19",
"@storybook/addon-interactions": "^6.4.19",
"@storybook/addon-links": "^6.4.19",
"@storybook/builder-webpack5": "^6.4.19",
"@storybook/jest": "^0.0.9",
"@storybook/manager-webpack5": "^6.4.19",
"@storybook/react": "^6.4.19",
"@storybook/test-runner": "^0.0.4",
"@storybook/testing-library": "^0.0.9",
"webpack": "^5.69.1",
"webpack-merge": "^5.8.0"

Environment

  • OS: Arch/Manjaro
  • Node.js version: v16.8.0
  • NPM version: 7.21.0
  • Browser (if applicable): Firefox
  • Browser version (if applicable): 98
  • Device (if applicable):

Additional context

Add any other context about the problem here.

[Bug] invalid path when running test-storybook

When attempting to run the code in this repo, I am getting the error:

Error: Cannot find module '../dist/cjs/util'
Require stack:
- /Users/jeremy/Sites/storybookjs/test-runner/bin/test-storybook.js
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:925:15)
    at Function.Module._load (node:internal/modules/cjs/loader:769:27)
    at Module.require (node:internal/modules/cjs/loader:997:19)
    at require (node:internal/modules/cjs/helpers:92:18)
    at Object.<anonymous> (/Users/jeremy/Sites/storybookjs/test-runner/bin/test-storybook.js:11:49)
    at Module._compile (node:internal/modules/cjs/loader:1108:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10)
    at Module.load (node:internal/modules/cjs/loader:973:32)
    at Function.Module._load (node:internal/modules/cjs/loader:813:14)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/Users/jeremy/Sites/storybookjs/test-runner/bin/test-storybook.js'
  ]
}
error Command failed with exit code 1.

Steps to reproduce the behavior

  1. clone repo
  2. run yarn storybook
  3. in a new terminal window, run yarn test-storybook

[Feature Request] Support mdx

All my stories are in mdx format.
It would be great if I could run all the play function tests therein automatically and during the ci pipeline.

I tried using --stories-json, but the runner still did not find any tests...

[Bug] Clean up `hasPlayFn`

Describe the bug

This is used by the test runner to link to the interactions addon in Storybook. However, we don't have this information in stories.json mode. Always linking to interactions will fix stories.json mode and also simplify the code significantly.

[Bug] page.evaluate: Execution context was destroyed, most likely because of a navigation.

Describe the bug

When multiple stories fail in the test runner, the following error occurs:

image

Notice that the first error report is correct, but the second is not.

I suspect the page instance is being shared between tests and if one breaks the rest of the tests will not have access to that instance correctly

Steps to reproduce the behavior

  1. Add a failure to a story play function, e.g.
FindBy.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  await canvas.findByRole('button');
-  await expect(true).toBe(true);
+  await expect(true).toBe(false);
};
  1. Run the test runner
  2. See error

Non-eject customization

Currently, to set up API hooks, you need to create a custom jest config.

Here's a strawman API for adding the API hooks that doesn't require ejecting:

// .storybook/test-runner.js
module.exports = {
  hooks: {
    preRender: (page, context) => { ... },
    postRender: (page, context) => { ... }
  },
}

Notes:

  • This allows us to change the test runner more easily in the future
  • This uses the .storybook directory, and maybe we should consider that for the jest config also
// jest-config.js
try {
  const { hooks } = require('.storybook/test-runner');
  ..
} catch(err) {}

[Bug] TypeError: (0 , _coreCommon.normalizeStories) is not a function

Describe the bug

Right after I run the command yarn test-storybook --url http://localhost:4400 this error appears. I have the storybook up and running.

Screenshot 2022-03-01 at 14 32 30

I was trying also with the stories.json file, so with the -s flag, but this returned another error:
Screenshot 2022-03-01 at 14 34 03

Environment

  • OS: MacOs Monterey
  • Node.js version: 14.17.5
  • NPM version: 6.14.4

Additional context

We are using the nrwl plugin for storybook - @nrwl/storybook in version 12.9.0 (unfortunately I can't update it at the moment).

I'm curious if it's a version issue like I need to update to the newest one? Because it's no matter if I provide a config path or not, the output is the same.

Eject command for customization

Currently customizing the jest config is a manual process. Add an eject command to:

  • Simplify customization
  • Make explicit that this kind of customization may be incompatible with future updates to the test runner

Todo:

  • Add comments to the config file explaining the "smarts"
  • Re-eject to get the most up-to-date config & diff against your previous changes

[Bug] Validation Error when running yarn test-storybook

Describe the bug

I am configuring my storybook to run test-runner following this guide:
Test runner guide

and when I run yarn test-storybook --url http://localhost:4400

I get an error.

yarn test-storybook --url http://localhost:4400
yarn run v1.22.17
$ test-storybook --url http://localhost:4400
● Validation Error:

  Option "testMatch" must be of type:
    array
  but instead received:
    string

  Example:
  {
    "testMatch": [
      "**/__tests__/**/*.[jt]s?(x)",
      "**/?(*.)+(spec|test).[jt]s?(x)"
    ]
  }

  Configuration Documentation:
  https://jestjs.io/docs/configuration

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Environment

  • OS: OSX
  • Node.js version: v16.13.2
  • NPM version: 8.1.2

Include '--testPathIgnorePatterns' in the CLI Options

I have an issue at the moment where some tests are failing, and I have decided to temporarily disable them so that the whole command doesn't fail.

Jest has a CLI option called --testPathIgnorePatterns=|[array] - but it doesn't seem to work with the test runner.

When I run it - I get:

test-storybook --testPathIgnorePatterns Button.stories.jsx

error: unknown option '--testPathIgnorePatterns'
Usage: test-storybook [options]

I'm not sure if I'm doing something wrong, or I've misunderstood how the test runner handles flags/options - I assumed that all jest options would be "carried through" to Jest.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.