-
-
Notifications
You must be signed in to change notification settings - Fork 237
feat: add storybook #1270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add storybook #1270
Changes from all commits
f77919f
2f85193
9063d55
bc318bd
2909a50
0543e94
ac5c501
927af6d
7d2266b
7cf2c33
f24ce4d
4478f7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,3 +44,6 @@ shared/types/lexicons | |
|
|
||
| # output | ||
| .vercel | ||
|
|
||
| *storybook.log | ||
| storybook-static | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| # Why are we using Storybook? | ||
|
|
||
| Storybook is a development environment for UI components that helps catch changes in UI while also having integrations for different kinds of tests. For testing, Storybook provides: | ||
|
|
||
| - **Accessibility tests** - Built-in a11y checks (link to example) | ||
| - **Visual tests** - Compare JPG screenshots (link to example) | ||
| - **Snapshot tests** - Compare HTML output (link to example) | ||
| - **Vitest tests** - Use stories directly in your unit tests (link to example) | ||
|
|
||
| ## Component Categories | ||
|
|
||
| We organize components into 3 categories. | ||
|
|
||
| ### UI Library Components | ||
|
|
||
| **Generic, reusable components** used throughout your application. | ||
|
|
||
| - Examples: Button, Input, Modal, Card | ||
| - **Testing focus:** Props, variants, accessibility | ||
| - **Coverage:** All variants and states | ||
|
|
||
| ### Composite Components | ||
|
|
||
| **Domain-specific components** built from UI library components. | ||
|
|
||
| - Examples: UserProfile, ProductCard, SearchForm | ||
| - **Testing focus:** Integration patterns, user interactions | ||
| - **Coverage:** Common usage scenarios | ||
|
|
||
| ### Page Components | ||
|
|
||
| **Full-page layouts** shown to end users. | ||
|
|
||
| - Examples: HomePage, Dashboard, CheckoutPage | ||
| - **Testing focus:** Layout, responsive behavior, integration testing | ||
| - **Coverage:** Critical user flows and breakpoints | ||
|
|
||
| ## Coverage Guidelines | ||
|
|
||
| ### Which Components Need Stories? | ||
|
|
||
| TBD | ||
|
|
||
| ### Convention | ||
|
|
||
| - An edge case per story | ||
| - Do not use `autodocs` | ||
|
|
||
| # How to Use | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this section reads to me like it's just a concise version of the Storybook docs, so I'm not sure it's valuable? The "Place a |
||
|
|
||
| ## Writing Stories | ||
|
|
||
| 1. **Create your first story** - Place a `.stories.ts` file next to your component: | ||
|
|
||
| ``` | ||
| components/ | ||
| ├── Button.vue | ||
| └── Button.stories.ts | ||
| ``` | ||
|
|
||
| 2. **Add the story code** - Each story file follows this pattern: | ||
|
|
||
| ```ts | ||
| // Button.stories.ts | ||
| import type { Meta, StoryObj } from '@nuxtjs/storybook' | ||
| import Component from './Button.vue' | ||
|
|
||
| const meta = { | ||
| component: Component, | ||
| // component configuration goes here | ||
| } satisfies Meta<typeof Component> | ||
|
|
||
| export default meta | ||
| type Story = StoryObj<typeof meta> | ||
|
|
||
| export const Default: Story = { | ||
| // story configuration goes here | ||
| } | ||
| ``` | ||
|
|
||
| 3. **Run Storybook locally:** | ||
|
|
||
| ```sh | ||
| pnpm storybook | ||
| ``` | ||
|
|
||
| 4. **Find your story** - Storybook URLs mirror your project structure. | ||
|
|
||
| For a component at `app/components/Button/Button.stories.ts`, the story will be available at `http://localhost:6006/?path=/story/components-button--default` | ||
|
|
||
| ## Configuration | ||
|
|
||
| ### Global Configuration (`.storybook/preview.ts`) | ||
|
|
||
| Affects all stories across the project: | ||
|
|
||
| ```ts | ||
| export default { | ||
| globals: { | ||
| locale: 'en-US', | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| ### Component Configuration (meta) | ||
|
|
||
| Overrides settings for a specific component: | ||
|
|
||
| ```ts | ||
| const meta = { | ||
| component: Button, | ||
| parameters: { | ||
| layout: 'centered', | ||
| }, | ||
| globals: { | ||
| locale: 'ja-JP', | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| ### Story Configuration | ||
|
|
||
| Overrides settings for individual stories: | ||
|
|
||
| ```ts | ||
| export const SpecialCase: Story = { | ||
| globals: { | ||
| locale: 'fr-FR', | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| ## Global App Settings | ||
|
|
||
| Global application settings are added to the Storybook toolbar for easy testing and viewing. Configure these in `.storybook/preview.ts` using the `globals` property with toolbar definitions. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import type { StorybookConfig } from '@nuxtjs/storybook' | ||
|
|
||
| const config = { | ||
| stories: ['../app/**/*.stories.@(js|ts|mdx)'], | ||
| addons: ['@storybook/addon-a11y', '@storybook/addon-docs'], | ||
| framework: '@storybook-vue/nuxt', | ||
| features: { | ||
| backgrounds: false, | ||
| }, | ||
| } satisfies StorybookConfig | ||
| export default config |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import type { Preview } from '@nuxtjs/storybook' | ||
| import { currentLocales } from '../config/i18n' | ||
| import { fn } from 'storybook/test' | ||
| import { ACCENT_COLORS } from '../shared/utils/constants' | ||
|
|
||
| // related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26 | ||
| // Stub Nuxt specific globals | ||
| // @ts-expect-error - dynamic global name | ||
| globalThis['__NUXT_COLOR_MODE__'] ??= { | ||
| preference: 'system', | ||
| value: 'dark', | ||
| getColorScheme: fn(() => 'dark'), | ||
| addColorScheme: fn(), | ||
| removeColorScheme: fn(), | ||
| } | ||
| // @ts-expect-error - dynamic global name | ||
| globalThis.defineOgImageComponent = fn() | ||
|
|
||
| const preview: Preview = { | ||
| parameters: { | ||
| controls: { | ||
| matchers: { | ||
| color: /(background|color)$/i, | ||
| date: /Date$/i, | ||
| }, | ||
| }, | ||
| }, | ||
| // Provides toolbars to switch things like theming and language | ||
| globalTypes: { | ||
| locale: { | ||
| name: 'Locale', | ||
| description: 'UI language', | ||
| defaultValue: 'en-US', | ||
| toolbar: { | ||
| icon: 'globe', | ||
| dynamicTitle: true, | ||
| items: [ | ||
| // English is at the top so it's easier to reset to it | ||
| { value: 'en-US', title: 'English (US)' }, | ||
| ...currentLocales | ||
| .filter(locale => locale.code !== 'en-US') | ||
| .map(locale => ({ value: locale.code, title: locale.name })), | ||
| ], | ||
| }, | ||
| }, | ||
| accentColor: { | ||
| name: 'Accent Color', | ||
| description: 'Accent color', | ||
| toolbar: { | ||
| icon: 'paintbrush', | ||
| dynamicTitle: true, | ||
| items: [ | ||
| ...Object.keys(ACCENT_COLORS.light).map(color => ({ | ||
| value: color, | ||
| title: color.charAt(0).toUpperCase() + color.slice(1), | ||
| })), | ||
| { value: undefined, title: 'No Accent' }, | ||
| ], | ||
| }, | ||
| }, | ||
| theme: { | ||
| name: 'Theme', | ||
| description: 'Color mode', | ||
| defaultValue: 'dark', | ||
| toolbar: { | ||
| icon: 'moon', | ||
| dynamicTitle: true, | ||
| items: [ | ||
| { value: 'light', icon: 'sun', title: 'Light' }, | ||
| { value: 'dark', icon: 'moon', title: 'Dark' }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| decorators: [ | ||
| (story, context) => { | ||
| const { locale, theme, accentColor } = context.globals as { | ||
| locale: string | ||
| theme: string | ||
| accentColor?: string | ||
| } | ||
|
|
||
| // Set theme from globals | ||
| document.documentElement.setAttribute('data-theme', theme) | ||
|
|
||
| // Set accent color from globals | ||
| if (accentColor) { | ||
| document.documentElement.style.setProperty('--accent-color', `var(--swatch-${accentColor})`) | ||
| } else { | ||
| document.documentElement.style.removeProperty('--accent-color') | ||
| } | ||
|
|
||
| return { | ||
| template: '<story />', | ||
| // Set locale from globals | ||
| created() { | ||
| if (this.$i18n) { | ||
| this.$i18n.setLocale(locale) | ||
| } | ||
| }, | ||
| updated() { | ||
| if (this.$i18n) { | ||
| this.$i18n.setLocale(locale) | ||
| } | ||
| }, | ||
| } | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
| export default preview |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import type { Meta, StoryObj } from '@nuxtjs/storybook' | ||
| import AppFooter from './AppFooter.vue' | ||
|
|
||
| const meta = { | ||
| component: AppFooter, | ||
| parameters: { | ||
| layout: 'fullscreen', | ||
| }, | ||
| globals: { | ||
| viewport: { value: undefined }, | ||
| }, | ||
| } satisfies Meta<typeof AppFooter> | ||
|
|
||
| export default meta | ||
| type Story = StoryObj<typeof meta> | ||
|
|
||
| export const Default: Story = {} | ||
|
|
||
| export const InContext: Story = { | ||
| render: () => ({ | ||
| components: { AppFooter }, | ||
| template: ` | ||
| <div style="display: flex; flex-direction: column; min-height: 100vh;"> | ||
| <div style="flex: 1; padding: 2rem;"> | ||
| <h1>Some page content</h1> | ||
| <span>See footer at the bottom</span> | ||
| </div> | ||
| <AppFooter /> | ||
| </div> | ||
| `, | ||
| }), | ||
| } | ||
|
|
||
| export const InLongPage: Story = { | ||
| render: () => ({ | ||
| components: { AppFooter }, | ||
| template: ` | ||
| <div style="display: flex; flex-direction: column; min-height: 100vh;"> | ||
| <div style="flex: 1; padding: 2rem;"> | ||
| <h1>Footer is all the way at the bottom!</h1> <br /> | ||
| ${Array.from({ length: 50 }, (_, i) => `<p key="${i}">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>`).join('')} | ||
| </div> | ||
| <AppFooter /> | ||
| </div> | ||
| `, | ||
| }), | ||
| } | ||
|
|
||
| export const MobileView: Story = { | ||
| ...InContext, | ||
| globals: { | ||
| viewport: { value: 'mobile1' }, | ||
| }, | ||
| } | ||
|
|
||
| export const TabletView: Story = { | ||
| ...InContext, | ||
| globals: { | ||
| viewport: { value: 'tablet' }, | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import type { Meta, StoryObj } from '@nuxtjs/storybook' | ||
| import AppHeader from './AppHeader.vue' | ||
|
|
||
| const meta = { | ||
| component: AppHeader, | ||
| parameters: { | ||
| layout: 'fullscreen', | ||
| }, | ||
| globals: { | ||
| viewport: { value: undefined }, | ||
| }, | ||
| } satisfies Meta<typeof AppHeader> | ||
|
|
||
| export default meta | ||
|
|
||
| export const Default: StoryObj<typeof AppHeader> = {} | ||
|
|
||
| export const Mobile: StoryObj<typeof AppHeader> = { | ||
| globals: { | ||
| viewport: { value: 'mobile1' }, | ||
| }, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on years of user feedback, I'd strongly advice against HTML snapshot testing. It usually ends up being mindlessly updating the snapshots again and again, without any real use. Visual tests are much better snapshots, as they are easier to review and more granular because the diff is on the pixel level and contains styling too.