feat: Add Storybook for component documentation, testing, and development (#34907)
Co-authored-by: Echo <ChaosExAnima@users.noreply.github.com> Co-authored-by: Renaud Chaput <renchap@gmail.com>
This commit is contained in:
parent
989ca63b59
commit
f2cfa4f482
17 changed files with 1822 additions and 104 deletions
40
.github/workflows/chromatic.yml
vendored
Normal file
40
.github/workflows/chromatic.yml
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
name: 'Chromatic'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- renovate/*
|
||||
- stable-*
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- '**/*.js'
|
||||
- '**/*.jsx'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '.github/workflows/chromatic.yml'
|
||||
|
||||
jobs:
|
||||
chromatic:
|
||||
name: Run Chromatic
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: Build Storybook
|
||||
run: yarn build-storybook
|
||||
|
||||
- name: Run Chromatic
|
||||
uses: chromaui/action@v12
|
||||
with:
|
||||
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
zip: true
|
||||
storybookBuildDir: 'storybook-static'
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -75,3 +75,6 @@ docker-compose.override.yml
|
|||
|
||||
# Ignore local-only rspec configuration
|
||||
.rspec-local
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
|
16
.storybook/main.ts
Normal file
16
.storybook/main.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../app/javascript/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-docs',
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-vitest',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
7
.storybook/manager.ts
Normal file
7
.storybook/manager.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { addons } from 'storybook/manager-api';
|
||||
|
||||
import theme from './storybook-theme';
|
||||
|
||||
addons.setConfig({
|
||||
theme,
|
||||
});
|
18
.storybook/preview-head.html
Normal file
18
.storybook/preview-head.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<style>
|
||||
/* Increase docs font size */
|
||||
.sbdocs.sbdocs-content :where(p:not(.sb-anchor, .sb-unstyled, .sb-unstyled p)),
|
||||
.sbdocs.sbdocs-content :where(li:not(.sb-anchor, .sb-unstyled, .sb-unstyled li)) {
|
||||
font-size: 1.0666rem; /* 17px */
|
||||
line-height: 1.585; /* 27px */
|
||||
}
|
||||
|
||||
.sbdocs.sbdocs-content :where(p:not(.sb-anchor, .sb-unstyled, .sb-unstyled p)) code,
|
||||
.sbdocs.sbdocs-content :where(li:not(.sb-anchor, .sb-unstyled, .sb-unstyled li)) code {
|
||||
font-size: 0.875rem; /* ~15px */
|
||||
}
|
||||
|
||||
/* Bring numbers back for ordered lists */
|
||||
ol {
|
||||
list-style: revert !important;
|
||||
}
|
||||
</style>
|
29
.storybook/preview.ts
Normal file
29
.storybook/preview.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import type { Preview } from '@storybook/react-vite';
|
||||
|
||||
// If you want to run the dark theme during development,
|
||||
// you can change the below to `/application.scss`
|
||||
import '../app/javascript/styles/mastodon-light.scss';
|
||||
|
||||
const preview: Preview = {
|
||||
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: 'todo',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
7
.storybook/storybook-addon-vitest.d.ts
vendored
Normal file
7
.storybook/storybook-addon-vitest.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
// The addon package.json incorrectly exports types, so we need to override them here.
|
||||
// See: https://github.com/storybookjs/storybook/blob/v9.0.4/code/addons/vitest/package.json#L70-L76
|
||||
declare module '@storybook/addon-vitest/vitest-plugin' {
|
||||
export * from '@storybook/addon-vitest/dist/vitest-plugin/index';
|
||||
}
|
||||
|
||||
export {};
|
7
.storybook/storybook-theme.ts
Normal file
7
.storybook/storybook-theme.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { create } from 'storybook/theming';
|
||||
|
||||
export default create({
|
||||
base: 'light',
|
||||
brandTitle: 'Mastodon Storybook',
|
||||
brandImage: 'https://joinmastodon.org/logos/wordmark-black-text.svg',
|
||||
});
|
8
.storybook/vitest.setup.ts
Normal file
8
.storybook/vitest.setup.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';
|
||||
import { setProjectAnnotations } from '@storybook/react-vite';
|
||||
|
||||
import * as projectAnnotations from './preview';
|
||||
|
||||
// This is an important step to apply the right configuration when testing your stories.
|
||||
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
||||
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
|
97
app/javascript/mastodon/components/button/button.stories.tsx
Normal file
97
app/javascript/mastodon/components/button/button.stories.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { fn, expect } from 'storybook/test';
|
||||
|
||||
import { Button } from '.';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
args: {
|
||||
secondary: false,
|
||||
compact: false,
|
||||
dangerous: false,
|
||||
disabled: false,
|
||||
onClick: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
text: {
|
||||
control: 'text',
|
||||
type: 'string',
|
||||
description:
|
||||
'Alternative way of specifying the button label. Will override `children` if provided.',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
control: 'text',
|
||||
table: {
|
||||
type: { summary: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['test'],
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const buttonTest: Story['play'] = async ({ args, canvas, userEvent }) => {
|
||||
await userEvent.click(canvas.getByRole('button'));
|
||||
await expect(args.onClick).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
const disabledButtonTest: Story['play'] = async ({
|
||||
args,
|
||||
canvas,
|
||||
userEvent,
|
||||
}) => {
|
||||
await userEvent.click(canvas.getByRole('button'));
|
||||
await expect(args.onClick).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: 'Primary button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
secondary: true,
|
||||
children: 'Secondary button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const Compact: Story = {
|
||||
args: {
|
||||
compact: true,
|
||||
children: 'Compact button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const Dangerous: Story = {
|
||||
args: {
|
||||
dangerous: true,
|
||||
children: 'Dangerous button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const PrimaryDisabled: Story = {
|
||||
args: {
|
||||
...Primary.args,
|
||||
disabled: true,
|
||||
},
|
||||
play: disabledButtonTest,
|
||||
};
|
||||
|
||||
export const SecondaryDisabled: Story = {
|
||||
args: {
|
||||
...Secondary.args,
|
||||
disabled: true,
|
||||
},
|
||||
play: disabledButtonTest,
|
||||
};
|
|
@ -22,6 +22,10 @@ interface PropsWithText extends BaseProps {
|
|||
|
||||
type Props = PropsWithText | PropsChildren;
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction that doesn't result in navigation.
|
||||
*/
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
type = 'button',
|
||||
onClick,
|
|
@ -12,6 +12,7 @@ import jsxA11Y from 'eslint-plugin-jsx-a11y';
|
|||
import promisePlugin from 'eslint-plugin-promise';
|
||||
import react from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import storybook from 'eslint-plugin-storybook';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
|
@ -187,6 +188,7 @@ export default tseslint.config([
|
|||
importPlugin.flatConfigs.react,
|
||||
// @ts-expect-error -- For some reason the formatjs package exports an empty object?
|
||||
formatjs.configs.strict,
|
||||
storybook.configs['flat/recommended'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
|
@ -252,6 +254,11 @@ export default tseslint.config([
|
|||
'app/javascript/mastodon/test_setup.js',
|
||||
'app/javascript/mastodon/test_helpers.tsx',
|
||||
'app/javascript/**/__tests__/**',
|
||||
'app/javascript/**/*.stories.ts',
|
||||
'app/javascript/**/*.stories.tsx',
|
||||
'app/javascript/**/*.test.ts',
|
||||
'app/javascript/**/*.test.tsx',
|
||||
'.storybook/**/*.ts',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -398,4 +405,18 @@ export default tseslint.config([
|
|||
globals: globals.vitest,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.stories.ts', '**/*.stories.tsx', '.storybook/**/*.ts'],
|
||||
rules: {
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['vitest.shims.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
24
package.json
24
package.json
|
@ -26,8 +26,12 @@
|
|||
"postinstall": "test -d node_modules/husky && husky || echo \"husky is not installed\"",
|
||||
"start": "node ./streaming/index.js",
|
||||
"test": "yarn lint && yarn run typecheck && yarn test:js run",
|
||||
"test:js": "vitest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"test:js": "vitest --project=legacy-tests",
|
||||
"test:storybook": "vitest --project=storybook",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "VITE_RUBY_PUBLIC_OUTPUT_DIR='.' VITE_RUBY_PUBLIC_DIR='./storybook-static' storybook build",
|
||||
"chromatic": "npx chromatic -d storybook-static"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -122,6 +126,10 @@
|
|||
"devDependencies": {
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@formatjs/cli": "^6.1.1",
|
||||
"@storybook/addon-a11y": "^9.0.4",
|
||||
"@storybook/addon-docs": "^9.0.4",
|
||||
"@storybook/addon-vitest": "^9.0.4",
|
||||
"@storybook/react-vite": "^9.0.4",
|
||||
"@testing-library/dom": "^10.2.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/emoji-mart": "3.0.14",
|
||||
|
@ -147,6 +155,10 @@
|
|||
"@types/react-toggle": "^4.0.3",
|
||||
"@types/redux-immutable": "^4.0.3",
|
||||
"@types/requestidlecallback": "^0.3.5",
|
||||
"@vitest/browser": "^3.2.1",
|
||||
"@vitest/coverage-v8": "^3.2.0",
|
||||
"@vitest/ui": "^3.2.1",
|
||||
"chromatic": "^12.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-import-resolver-typescript": "^4.2.5",
|
||||
"eslint-plugin-formatjs": "^5.3.1",
|
||||
|
@ -156,11 +168,14 @@
|
|||
"eslint-plugin-promise": "~7.2.1",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-storybook": "^9.0.4",
|
||||
"globals": "^16.0.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^16.0.0",
|
||||
"playwright": "^1.52.0",
|
||||
"prettier": "^3.3.3",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"storybook": "^9.0.4",
|
||||
"stylelint": "^16.19.1",
|
||||
"stylelint-config-prettier-scss": "^1.0.0",
|
||||
"stylelint-config-standard-scss": "^15.0.1",
|
||||
|
@ -168,13 +183,14 @@
|
|||
"typescript-eslint": "^8.29.1",
|
||||
"vite-plugin-rails": "^0.5.0",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "^3.1.3"
|
||||
"vitest": "^3.2.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18.2.7",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"kind-of": "^6.0.3",
|
||||
"vite-plugin-ruby": "^5.1.0"
|
||||
"vite-plugin-ruby": "^5.1.0",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
|
|
|
@ -27,9 +27,12 @@
|
|||
},
|
||||
"include": [
|
||||
"vite.config.mts",
|
||||
"vitest.config.mts",
|
||||
"config/vite",
|
||||
"app/javascript/mastodon",
|
||||
"app/javascript/entrypoints",
|
||||
"app/javascript/types"
|
||||
"app/javascript/types",
|
||||
".storybook/*.ts",
|
||||
".storybook/*.tsx"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,26 +1,69 @@
|
|||
import { configDefaults, defineConfig } from 'vitest/config';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import {
|
||||
configDefaults,
|
||||
defineConfig,
|
||||
TestProjectInlineConfiguration,
|
||||
} from 'vitest/config';
|
||||
|
||||
import { config as viteConfig } from './vite.config.mjs';
|
||||
|
||||
const storybookTests: TestProjectInlineConfiguration = {
|
||||
plugins: [
|
||||
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
|
||||
storybookTest({
|
||||
configDir: '.storybook',
|
||||
storybookScript: 'yarn run storybook',
|
||||
}),
|
||||
react(),
|
||||
svgr(),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
test: {
|
||||
name: 'storybook',
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: 'playwright',
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
setupFiles: [resolve(__dirname, '.storybook/vitest.setup.ts')],
|
||||
},
|
||||
};
|
||||
|
||||
const legacyTests: TestProjectInlineConfiguration = {
|
||||
extends: true,
|
||||
test: {
|
||||
name: 'legacy-tests',
|
||||
environment: 'jsdom',
|
||||
include: [
|
||||
...configDefaults.include,
|
||||
'**/__tests__/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
],
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
'**/node_modules/**',
|
||||
'vendor/**',
|
||||
'config/**',
|
||||
'log/**',
|
||||
'public/**',
|
||||
'tmp/**',
|
||||
],
|
||||
globals: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig(async (context) => {
|
||||
const baseConfig = await viteConfig(context);
|
||||
|
||||
return {
|
||||
...(await viteConfig(context)),
|
||||
...baseConfig,
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: [
|
||||
...configDefaults.include,
|
||||
'**/__tests__/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
],
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
'**/node_modules/**',
|
||||
'vendor/**',
|
||||
'config/**',
|
||||
'log/**',
|
||||
'public/**',
|
||||
'tmp/**',
|
||||
],
|
||||
globals: true,
|
||||
projects: [legacyTests, storybookTests],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
1
vitest.shims.d.ts
vendored
Normal file
1
vitest.shims.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="@vitest/browser/providers/playwright" />
|
Loading…
Add table
Add a link
Reference in a new issue