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