diff --git a/.eslintrc.js b/.eslintrc.js index 6753607da287..d54d045ced36 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,6 +13,7 @@ module.exports = { 'plugin:cypress/recommended', 'prettier', 'plugin:import/recommended', + 'plugin:storybook/recommended', ], env: { es6: true, @@ -87,8 +88,10 @@ module.exports = { jsx: true, }, }, + plugins: ['@typescript-eslint', 'babel', '@emotion', 'cypress', 'unicorn'], rules: { 'no-duplicate-imports': [0], // handled by @typescript-eslint + 'react/prop-types': [0], '@typescript-eslint/ban-types': [0], // TODO enable in future '@typescript-eslint/no-non-null-assertion': [0], '@typescript-eslint/consistent-type-imports': 'error', diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index abf00df7e748..faca4c1d17a2 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -7,6 +7,10 @@ on: pull_request: types: [opened, synchronize, reopened] +# Note: Needs write permimission to upload __image_snapshots__ folder +permissions: + contents: write + jobs: changes: runs-on: ubuntu-latest @@ -56,6 +60,7 @@ jobs: e2e-with-cypress: needs: [changes, build] + timeout-minutes: 60 runs-on: ubuntu-latest strategy: @@ -98,3 +103,27 @@ jobs: path: | cypress/screenshots cypress/videos + + component-test-with-storybook: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + fail-fast: false + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js { matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + check-latest: true + - name: Install dependencies + run: npm install + - name: Install Playwright + run: npx playwright install --with-deps + - name: Build Storybook + run: npm run build:storybook + - name: Run component tests + run: npm run test:component diff --git a/.gitignore b/.gitignore index 5a36cf21b91b..4813f4bfc432 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ bin/ public/ node_modules/ npm-debug.log +*.tsbuildinfo .DS_Store .tern-project yarn-error.log @@ -13,6 +14,7 @@ manifest.yml cypress/videos cypress/screenshots __diff_output__ +__image_snapshots__ coverage/ .cache *.log diff --git a/.storybook/main.ts b/.storybook/main.ts index b987eeefc1eb..3f4754cff58d 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -11,21 +11,21 @@ function getAbsolutePath(value: string): any { } const config: StorybookConfig = { - stories: ['../packages/**/src/**/?(*.)(story|stories).(js|jsx|ts|tsx)'], + stories: ['../packages/**/src/**/?(*.)(story|stories).(js|jsx|ts|tsx|mdx)'], + addons: [ getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('@storybook/addon-interactions'), getAbsolutePath('@storybook/addon-storysource'), getAbsolutePath('@storybook/addon-a11y'), getAbsolutePath('storybook-addon-deep-controls'), getAbsolutePath('storybook-dark-mode'), + getAbsolutePath('@storybook/addon-webpack5-compiler-babel'), ], + framework: { name: getAbsolutePath('@storybook/react-webpack5'), options: {}, }, - docs: { - autodocs: 'tag', - }, }; - export default config; diff --git a/.storybook/preview.jsx b/.storybook/preview.jsx deleted file mode 100644 index 8234b2c89cd9..000000000000 --- a/.storybook/preview.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { DocsContainer } from '@storybook/addon-docs'; -import { themes } from '@storybook/theming'; -import { useDarkMode } from 'storybook-dark-mode'; -import { ThemeProvider } from '@emotion/react'; -import { lightTheme, darkTheme, UIProvider, GlobalStyles } from 'decap-cms-ui-next/src'; -import themeViewports from './viewports'; -import brandTheme from './theme'; - -function ThemeWrapper({ children }) { - const darkMode = useDarkMode(); - const theme = darkMode ? { darkMode, ...darkTheme } : { darkMode, ...lightTheme }; - - return ( - - - - {children} - - - ); -} - -export const parameters = { - layout: 'centered', - viewport: { - viewports: { - ...themeViewports, - }, - }, - actions: { argTypesRegex: '^on.*' }, - options: { - showPanel: true, - storySort: { - method: 'alphabetical', - order: ['Pages', 'Components', 'Widgets'], - }, - }, - deepControls: { enabled: true }, - darkMode: { - dark: { ...themes.dark, ...brandTheme.dark }, - light: { ...themes.normal, ...brandTheme.light }, - }, - docs: { - container: props => { - const isDark = useDarkMode(); - const currentProps = { ...props }; - currentProps.theme = isDark - ? { ...themes.dark, ...brandTheme.dark } - : { ...themes.normal, ...brandTheme.light }; - return React.createElement(DocsContainer, currentProps); - }, - }, -}; - -export const decorators = [renderStory => {renderStory()}]; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 000000000000..972faa458550 --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Preview } from '@storybook/react'; +import { DocsContainer } from '@storybook/addon-docs'; +import { themes } from '@storybook/theming'; +import { useDarkMode } from 'storybook-dark-mode'; +import { ThemeProvider } from '@emotion/react'; +import { I18n } from 'react-polyglot'; +import { en } from 'decap-cms-locales'; +import { lightTheme, darkTheme, UIProvider, GlobalStyles } from 'decap-cms-ui-next'; +import themeViewports from './viewports'; +import brandTheme from './theme'; + +const preview: Preview = { + decorators: [ + Story => { + const darkMode = useDarkMode(); + const theme = darkMode ? { darkMode, ...darkTheme } : { darkMode, ...lightTheme }; + + return ( + + + + + + + + + + ); + }, + ], + parameters: { + layout: 'centered', + viewport: { + viewports: { + ...themeViewports, + }, + }, + options: { + showPanel: true, + storySort: { + method: 'alphabetical', + order: ['Foundations', 'Pages', 'Components', 'Fields'], + }, + }, + deepControls: { enabled: true }, + darkMode: { + dark: { ...themes.dark, ...brandTheme.dark }, + light: { ...themes.normal, ...brandTheme.light }, + }, + docs: { + container: props => { + const isDark = useDarkMode(); + const currentProps = { ...props }; + currentProps.theme = isDark + ? { ...themes.dark, ...brandTheme.dark } + : { ...themes.normal, ...brandTheme.light }; + return React.createElement(DocsContainer, currentProps); + }, + }, + }, +}; + +export default preview; diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts new file mode 100644 index 000000000000..2f5f77a9e489 --- /dev/null +++ b/.storybook/test-runner.ts @@ -0,0 +1,38 @@ +import type { TestRunnerConfig } from '@storybook/test-runner'; +import { injectAxe, checkA11y } from 'axe-playwright'; + +import { waitForPageReady } from '@storybook/test-runner'; + +import { toMatchImageSnapshot } from 'jest-image-snapshot'; + +const config: TestRunnerConfig = { + setup() { + expect.extend({ toMatchImageSnapshot }); + }, + async preVisit(page) { + await injectAxe(page); + }, + async postVisit(page) { + // Awaits for the page to be loaded and available including assets (e.g., fonts) + // await waitForPageReady(page); + + // Checks for accessibility issues + await checkA11y(page, '#storybook-root', { + detailedReport: true, + detailedReportOptions: { + html: true, + }, + }); + + // Generates a DOM snapshot file based on the story identifier + const elementHandler = await page.$('#storybook-root'); + const innerHTML = await elementHandler.innerHTML(); + expect(innerHTML).toMatchSnapshot(); + + // Generates a Visual snapshot file based on the story identifier + const image = await elementHandler.screenshot(); + expect(image).toMatchImageSnapshot(); + }, +}; + +export default config; diff --git a/cypress.config.ts b/cypress.config.ts index 6beb7c1b6c4e..3ed76b4d6b8f 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -2,10 +2,12 @@ import { defineConfig } from 'cypress'; export default defineConfig({ projectId: '1c35bs', + retries: { runMode: 4, openMode: 0, }, + e2e: { video: false, // We've imported your old cypress plugins here. @@ -15,6 +17,18 @@ export default defineConfig({ return require('./cypress/plugins/index.js')(on, config); }, baseUrl: 'http://localhost:8080', - specPattern: 'cypress/e2e/*spec*.js', + specPattern: [ + 'cypress/e2e/*spec*.js', + 'cypress/component/**/*.spec.cy.{js,jsx,ts,tsx}', + 'packages/decap-cms-ui-next/src/**/__tests__/*.spec.cy.{js,jsx,ts,tsx}', + ], }, + + // component: { + // devServer: { + // framework: 'react', + // bundler: 'webpack', + // webpackConfig: 'scripts/webpack.js', + // }, + // }, }); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 000000000000..698b01a42c35 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,37 @@ +/// +// *********************************************** +// This example commands.ts 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 will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } \ No newline at end of file diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html new file mode 100644 index 000000000000..ac6e79fd83df --- /dev/null +++ b/cypress/support/component-index.html @@ -0,0 +1,12 @@ + + + + + + + Components App + + +
+ + \ No newline at end of file diff --git a/cypress/support/component.ts b/cypress/support/component.ts new file mode 100644 index 000000000000..37f59edbe5fc --- /dev/null +++ b/cypress/support/component.ts @@ -0,0 +1,39 @@ +// *********************************************************** +// This example support/component.ts 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') + +import { mount } from 'cypress/react18' + +// Augment the Cypress namespace to include type definitions for +// your custom command. +// Alternatively, can be defined in cypress/support/component.d.ts +// with a at the top of your spec. +declare global { + namespace Cypress { + interface Chainable { + mount: typeof mount + } + } +} + +Cypress.Commands.add('mount', mount) + +// Example use: +// cy.mount() \ No newline at end of file diff --git a/dev-test/backends/azure/index.html b/dev-test/backends/azure/index.html index 58e4528b914e..2297f105d960 100644 --- a/dev-test/backends/azure/index.html +++ b/dev-test/backends/azure/index.html @@ -2,6 +2,10 @@ + Decap CMS Development Test diff --git a/dev-test/backends/bitbucket/index.html b/dev-test/backends/bitbucket/index.html index 58e4528b914e..2297f105d960 100644 --- a/dev-test/backends/bitbucket/index.html +++ b/dev-test/backends/bitbucket/index.html @@ -2,6 +2,10 @@ + Decap CMS Development Test diff --git a/dev-test/backends/git-gateway/index.html b/dev-test/backends/git-gateway/index.html index 58e4528b914e..2297f105d960 100644 --- a/dev-test/backends/git-gateway/index.html +++ b/dev-test/backends/git-gateway/index.html @@ -2,6 +2,10 @@ + Decap CMS Development Test diff --git a/dev-test/backends/gitea/index.html b/dev-test/backends/gitea/index.html index dc20859bd218..f6d9eb6d7558 100644 --- a/dev-test/backends/gitea/index.html +++ b/dev-test/backends/gitea/index.html @@ -2,6 +2,10 @@ + Decap CMS Development Test diff --git a/dev-test/backends/github/index.html b/dev-test/backends/github/index.html index 58e4528b914e..2297f105d960 100644 --- a/dev-test/backends/github/index.html +++ b/dev-test/backends/github/index.html @@ -2,6 +2,10 @@ + Decap CMS Development Test diff --git a/dev-test/backends/gitlab/index.html b/dev-test/backends/gitlab/index.html index 58e4528b914e..2297f105d960 100644 --- a/dev-test/backends/gitlab/index.html +++ b/dev-test/backends/gitlab/index.html @@ -2,6 +2,10 @@ + Decap CMS Development Test diff --git a/dev-test/backends/proxy/index.html b/dev-test/backends/proxy/index.html index 58e4528b914e..2297f105d960 100644 --- a/dev-test/backends/proxy/index.html +++ b/dev-test/backends/proxy/index.html @@ -2,6 +2,10 @@ + Decap CMS Development Test diff --git a/dev-test/backends/test/index.html b/dev-test/backends/test/index.html index 8147bcc0118d..664c8e3cb652 100644 --- a/dev-test/backends/test/index.html +++ b/dev-test/backends/test/index.html @@ -2,6 +2,10 @@ + Decap CMS Development Test