mirror of https://github.com/mastodon/mastodon
				
				
				
			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>pull/34959/head
							parent
							
								
									989ca63b59
								
							
						
					
					
						commit
						f2cfa4f482
					
				@ -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'
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
import { addons } from 'storybook/manager-api';
 | 
			
		||||
 | 
			
		||||
import theme from './storybook-theme';
 | 
			
		||||
 | 
			
		||||
addons.setConfig({
 | 
			
		||||
  theme,
 | 
			
		||||
});
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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 {};
 | 
			
		||||
@ -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',
 | 
			
		||||
});
 | 
			
		||||
@ -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]);
 | 
			
		||||
@ -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,
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
/// <reference types="@vitest/browser/providers/playwright" />
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue