diff --git a/.circleci/config.yml b/.circleci/config.yml index 1eb5a6b1b4a4..389a649622dc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,4 @@ -version: 2 +version: 2.1 aliases: - &defaults @@ -6,9 +6,6 @@ aliases: docker: - image: circleci/node:10 -dependencies: - pre: - - yarn global add npm jobs: build: <<: *defaults @@ -17,7 +14,7 @@ jobs: - restore_cache: name: Restore core dependencies cache keys: - - core-dependencies-v3-{{ checksum "yarn.lock" }} + - core-dependencies-v4-{{ checksum "yarn.lock" }} - run: name: Install dependencies command: yarn install @@ -29,9 +26,11 @@ jobs: command: yarn bootstrap --core - save_cache: name: Cache core dependencies - key: core-dependencies-v3-{{ checksum "yarn.lock" }} + key: core-dependencies-v4-{{ checksum "yarn.lock" }} paths: - - ~/.cache/yarn + - ~/.cache + - node_modules + - /root/.cache - persist_to_workspace: root: . paths: @@ -47,9 +46,6 @@ jobs: - checkout - attach_workspace: at: . - - run: - name: Generate static examples - command: yarn build-storybooks - run: name: Run chromatic on the pre-built storybook command: yarn chromatic @@ -66,97 +62,50 @@ jobs: yarn packtracker examples: <<: *defaults + parallelism: 4 steps: - checkout - attach_workspace: at: . - run: - name: Workaround for https://github.com/GoogleChrome/puppeteer/issues/290 - command: sh ./scripts/workaround-puppeteer-issue-290.sh - - run: - name: Build react kitchen-sink - command: | - cd examples/cra-kitchen-sink - yarn build-storybook - - run: - name: Build react typescript kitchen-sink - command: | - cd examples/cra-ts-kitchen-sink - yarn build-storybook - - run: - name: Build vue kitchen-sink - command: | - cd examples/vue-kitchen-sink - yarn build-storybook - - run: - name: Build svelte kitchen-sink - command: | - cd examples/svelte-kitchen-sink - yarn build-storybook - - run: - name: Build angular-cli + name: examples command: | - cd examples/angular-cli - yarn build-storybook - - run: - name: Build ember-cli - command: | - cd examples/ember-cli - yarn build-storybook - - run: - name: Build polymer-cli - command: | - cd examples/polymer-cli - yarn build-storybook - - run: - name: Build marko-cli - command: | - cd examples/marko-cli - yarn build-storybook - - run: - name: Build mithril kitchen-sink - command: | - cd examples/mithril-kitchen-sink - yarn build-storybook - - run: - name: Build html kitchen-sink - command: | - cd examples/html-kitchen-sink - yarn build-storybook - - run: - name: Build riot kitchen-sink - command: | - cd examples/riot-kitchen-sink - yarn build-storybook - - run: - name: Build preact kitchen-sink - command: | - cd examples/preact-kitchen-sink - yarn build-storybook - - run: - name: Build cra react15 - command: | - cd examples/cra-react15 - yarn build-storybook - - run: - name: Build official-storybook - command: | - cd examples/official-storybook - yarn build-storybook - # - run: - # name: Run image snapshots - # command: yarn test --image - - store_artifacts: - path: examples/official-storybook/image-snapshots/__image_snapshots__ - destination: official_storybook_image_snapshots + yarn build-storybooks - persist_to_workspace: root: . paths: + - built-storybooks + e2e: + working_directory: /tmp/storybook + docker: + - image: cypress/base:8 + environment: + TERM: xterm + steps: + - checkout + - attach_workspace: + at: . + - run: + name: install cypress + command: yarn cypress install + - save_cache: + name: Cache core dependencies + key: core-dependencies-v4-{{ checksum "yarn.lock" }} + paths: + - ~/.cache - node_modules - - examples - - addons - - app - - lib + - /root/.cache + - run: + name: running example + command: yarn serve-storybooks + background: true + - run: + name: await running examples + command: yarn await-serve-storybooks + - run: + name: cypress run + command: yarn cypress run + smoke-tests: <<: *defaults steps: @@ -253,18 +202,13 @@ jobs: - restore_cache: name: Restore core dependencies cache keys: - - core-dependencies-v3-{{ checksum "yarn.lock" }} + - core-dependencies-v4-{{ checksum "yarn.lock" }} - run: name: Install dependencies - command: yarn install + command: yarn bootstrap --install - run: name: Trigger build command: ./scripts/build-frontpage.js - - save_cache: - name: Cache core dependencies - key: core-dependencies-v3-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn docs: <<: *defaults steps: @@ -287,12 +231,7 @@ jobs: name: Cache docs dependencies key: docs-dependencies-v2-{{ checksum "docs/yarn.lock" }} paths: - - ~/.cache/yarn - - persist_to_workspace: - root: . - paths: - - docs/public - - docs/node_modules + - ~/.cache lint: <<: *defaults steps: @@ -325,19 +264,18 @@ jobs: name: Upload coverage command: yarn coverage workflows: - version: 2 - build_test_deploy: + test: jobs: - build - - docs - - frontpage - lint: requires: - - docs - build - examples: requires: - build + - e2e: + requires: + - examples - smoke-tests: requires: - build @@ -355,4 +293,8 @@ workflows: - test - chromatic: requires: - - build + - examples + deploy: + jobs: + - docs + - frontpage diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 6481b025e3ce..26e640026d7f 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -7,17 +7,11 @@ jobs: name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }} runs-on: ${{ matrix.os }} - strategy: - matrix: - node-version: [10] - os: [ubuntu-latest] - steps: - uses: actions/checkout@v1 - - name: Use Node.js ${{ matrix.node_version }} - uses: actions/setup-node@v1 + - uses: actions/setup-node@v1 with: - version: ${{ matrix.node_version }} + node-version: '10.x' - name: install, bootstrap run: | yarn bootstrap --core diff --git a/.gitignore b/.gitignore index 124238c64b88..48f61b11955e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ htpasswd storybook-out /addons/docs/common/config-* built-storybooks +cypress/videos +cypress/screenshots diff --git a/cypress.json b/cypress.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/cypress.json @@ -0,0 +1 @@ +{} diff --git a/cypress/.eslintrc.json b/cypress/.eslintrc.json new file mode 100644 index 000000000000..373914272e56 --- /dev/null +++ b/cypress/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "plugin:cypress/recommended" + ] +} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 000000000000..da18d9352a17 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/helper.ts b/cypress/helper.ts new file mode 100644 index 000000000000..61c9920125e4 --- /dev/null +++ b/cypress/helper.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable jest/valid-expect */ +const baseUrl = 'http://localhost:8001'; + +type StorybookApps = 'official-storybook'; + +type Addons = 'Knobs'; + +export const visitExample = (app: StorybookApps, route = '') => { + return cy + .clearLocalStorage() + .visit(`${baseUrl}/${app}/${route}`) + .get(`#storybook-preview-iframe`) + .then({ timeout: 10000 }, iframe => { + return cy.wrap(iframe).should(() => { + const content: Document | null = (iframe[0] as HTMLIFrameElement).contentDocument; + const element: HTMLElement | null = content !== null ? content.documentElement : null; + + expect(element).not.null; + + if (element !== null) { + expect(element.querySelector('#root > *')).not.null; + } + }); + }); +}; + +export const clickAddon = (addonName: Addons) => { + return cy + .get(`[role=tablist] button[role=tab]`) + .contains(addonName) + .click(); +}; + +export const getStorybookPreview = () => { + return cy.get(`#storybook-preview-iframe`).then({ timeout: 10000 }, iframe => { + const content: Document | null = (iframe[0] as HTMLIFrameElement).contentDocument; + const element: HTMLElement | null = content !== null ? content.documentElement : null; + + console.log({ element, content, iframe }); + + return cy + .wrap(iframe) + .should(() => { + expect(element).not.null; + + if (element !== null) { + expect(element.querySelector('#root > *')).not.null; + } + }) + .then(() => { + return cy.wrap(element).get('#root'); + }); + }); +}; diff --git a/cypress/integration/knobs.spec.ts b/cypress/integration/knobs.spec.ts new file mode 100644 index 000000000000..a670ef486b22 --- /dev/null +++ b/cypress/integration/knobs.spec.ts @@ -0,0 +1,21 @@ +import { clickAddon, visitExample } from '../helper'; + +describe('Knobs', () => { + beforeEach(() => { + visitExample('official-storybook', '?path=/story/addons-knobs-withknobs--tweaks-static-values'); + }); + + it('[text] it should change a string value', () => { + clickAddon('Knobs'); + + cy.get('#Name') + .clear() + .type('John Doe'); + + cy.preview() + .console('info') + .find('p') + .eq(0) + .should('contain.text', 'My name is John Doe'); + }); +}); diff --git a/cypress/integration/navigation.spec.ts b/cypress/integration/navigation.spec.ts new file mode 100644 index 000000000000..942cf47cc643 --- /dev/null +++ b/cypress/integration/navigation.spec.ts @@ -0,0 +1,40 @@ +import { visitExample } from '../helper'; + +describe('Navigation', () => { + beforeEach(() => { + visitExample('official-storybook'); + }); + + it('should search navigation item', () => { + cy.get('#storybook-explorer-searchfield') + .click() + .type('persisting the action logger'); + + cy.get('.sidebar-container a') + .should('contain', 'Persisting the action logger') + .and('not.contain', 'a11y'); + }); + + it('should display no results after searching a non-existing navigation item', () => { + cy.get('#storybook-explorer-searchfield') + .click() + .type('zzzzzzzzzz'); + + cy.get('.sidebar-container').should('contain', 'This filter resulted in 0 results'); + }); +}); + +describe('Routing', () => { + it('should navigate to story addons-a11y-basebutton--default', () => { + visitExample('official-storybook'); + cy.get('#exploreraddons-a11y-basebutton--label').click(); + + cy.url().should('include', 'path=/story/addons-a11y-basebutton--label'); + }); + + it('should directly visit a certain story and render correctly', () => { + visitExample('official-storybook', '?path=/story/addons-a11y-basebutton--label'); + + cy.preview().should('contain.text', 'Testing the a11y addon'); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 000000000000..e699dc68b670 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,22 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +const wp = require('@cypress/webpack-preprocessor'); +const webpackConfig = require('./webpack.config'); + +module.exports = on => { + const options = { + webpackOptions: webpackConfig, + }; + on('file:preprocessor', wp(options)); +}; diff --git a/cypress/plugins/webpack.config.js b/cypress/plugins/webpack.config.js new file mode 100644 index 000000000000..01774bb3d143 --- /dev/null +++ b/cypress/plugins/webpack.config.js @@ -0,0 +1,21 @@ +module.exports = { + resolve: { + extensions: ['.ts', '.js'], + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: [/node_modules/], + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + }, + }, + ], + }, + ], + }, +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 000000000000..d7377c5df89d --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,59 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable jest/valid-expect */ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +const logger = console; +Cypress.Commands.add( + 'console', + { + prevSubject: true, + }, + (subject, method = 'log') => { + logger[method]('The subject is', subject); + return subject; + } +); + +Cypress.Commands.add('preview', {}, () => { + return cy.get(`#storybook-preview-iframe`).then({ timeout: 10000 }, iframe => { + const content = iframe[0].contentDocument; + const element = content !== null ? content.documentElement : null; + + return cy + .wrap(iframe) + .should(() => { + expect(element).not.null; + + if (element !== null) { + expect(element.querySelector('#root > *')).not.null; + } + }) + .then(() => { + return element.querySelector('#root'); + }); + }); +}); diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 000000000000..37a498fb5bf3 --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 000000000000..6375e4c73dfa --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "baseUrl": "../node_modules", + "target": "es5", + "lib": ["es2017", "dom"], + "types": ["cypress"] + }, + "include": ["**/*.ts"] +} diff --git a/examples/ember-cli/package.json b/examples/ember-cli/package.json index 95110f343d5e..059f45fad102 100644 --- a/examples/ember-cli/package.json +++ b/examples/ember-cli/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "build": "ember build", - "prebuild-storybook": "yarn build && cp -r public/* dist", + "prebuild-storybook": "yarn build && shx cp -r public/* dist", "build-storybook": "build-storybook -s dist", "dev": "ember serve", "storybook": "yarn build && start-storybook -p 9009 -s dist", diff --git a/examples/official-storybook/stories/app-acceptance.stories.js b/examples/official-storybook/stories/app-acceptance.stories.js deleted file mode 100644 index edea0adc9125..000000000000 --- a/examples/official-storybook/stories/app-acceptance.stories.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { storiesOf } from '@storybook/react'; - -// For these stories to work, you must build the static version of the -// example storybooks *before* running this storybook. - -const chapter = storiesOf('App|acceptance', module); - -const style = { - border: 0, - position: 'absolute', - top: 0, - left: 0, - width: '100vw', - height: '100vh', -}; - -[ - 'cra-kitchen-sink', - 'cra-ts-kitchen-sink', - 'vue-kitchen-sink', - 'svelte-kitchen-sink', - 'angular-cli', - 'polymer-cli', - 'mithril-kitchen-sink', - 'html-kitchen-sink', - 'riot-kitchen-sink', - 'preact-kitchen-sink', - 'cra-react15', -].forEach(name => { - chapter.add(name, () =>