Files
b1732dbfe1 feat(storybook): Vite-based Storybook with full coverage and visual regression testing (#7090)
* docs: add specification for shadcn storybook migration

Add comprehensive planning documents for design system migration:
- spec.md: Feature specification with 5 user stories
- plan.md: Implementation plan with technical context
- research.md: MSW, Chromatic, and story pattern findings
- data-model.md: Component inventory schema
- tasks.md: 117 implementation tasks organized by user story
- quickstart.md: Developer guide for creating stories
- contracts/: Story templates for UI, common, and page components

This establishes the foundation for systematic Storybook coverage
to enable designer collaboration on the shadcn migration.

* feat: add MSW fixture infrastructure for Storybook stories

## MSW Infrastructure
- Add fixture-based handlers using real API data from staging CGW
- Create fixtures for balances, portfolio, positions, safes, chains
- Support 5 scenarios: efSafe, vitalik, spamTokens, safeTokenHolder, empty
- Add feature flag testing via chain config overrides (POSITIONS, PORTFOLIO_ENDPOINT)
- Include fetch-fixtures.ts script to refresh fixture data

## Storybook Decorators
- Add LayoutDecorator for page-level stories with sidebar/header
- Add MockProviderDecorator for Redux/context providers

## Component Inventory Tool
- Create AST-based scanner for component discovery
- Add coverage tracking and dependency analysis
- Implement priority scoring for story creation order
- Add inventory and coverage-report CLI commands

## Documentation Updates
- Update plan.md, spec.md, tasks.md to reflect fixture-based approach
- Update quickstart.md with fixtureHandlers usage examples

* chore: complete Phase 4 MSW fixture expansion (User Story 2)

- Add Safe Apps fixtures fetched from staging CGW (mainnet.json)
- Add createSafeAppsHandlersFromFixture to MSW handlers
- Update createHandlersFromFixture to include Safe Apps by default
- Create dependency-audit.ts script for analyzing fixture coverage gaps
- Add msw-fixtures.md documentation for fixture system
- Fix inventory scanner path resolution for workspace execution
- Rename scripts to storybook:inventory, storybook:coverage, storybook:dependencies

Phase 4 checkpoint complete:
- Core fixtures: balances, portfolio, positions, safes, chains, safe-apps
- Utility handlers: transactions, auth, relay, messages, web3 RPC
- Scripts: storybook:inventory, storybook:coverage, storybook:dependencies

* docs: remove shadcn references, focus on existing component coverage

- Rename spec from "shadcn migration" to "Storybook Coverage Expansion"
- Remove all shadcn/ui component tasks (will be added in future phase)
- Expand Phase 5 tasks to cover existing MUI components by category
- Update component counts to match current inventory (330 total, 14 with stories)
- Fix Storybook version: 8.x → 10.x
- Add current coverage status section to tasks.md

* feat: add sidebar component stories (T042-T044)

- SafeHeaderInfo.stories.tsx: Default, Loading, LargeBalance, ZeroBalance,
  MultipleOwners, Counterfactual, PolygonChain variants
- MultiAccountContextMenu.stories.tsx: Default, WithoutAddNetwork,
  MultipleChains, LongName, NoName, TestnetOnly variants
- QrModal.stories.tsx: Default, WithChainPrefix, and multiple network
  variants (Polygon, Arbitrum, Optimism, Base)

Phase 5.1 complete - sidebar stories now enable page-level stories.

* docs: add MSW best practices for Storybook stories

- Document regex URL patterns (wildcard strings unreliable in MSW v2)
- Add mswLoader requirement for docs mode
- Document RTK Query requirements (deployed: true, loaded: true)
- Update story template with MSW best practices and valid TS syntax
- Fix AssetsTable story to work with MSW fixtures
- Add mockServiceWorker.js to eslint ignores
- Fix MSW index exports to only include working fixtures

* feat: add real component stories for Dashboard, Assets, Overview, PendingTxs, SafeAppList, and OwnerList

- Dashboard stories render real Dashboard component directly (no mock layout)
- Overview stories include wallet connected and no wallet states
- OwnerList stories include ENS names and address book entries
- Assets and PendingTxs stories with realistic MSW fixtures
- SafeAppList stories with real app data
- Add localhost:6006 to OFFICIAL_HOSTS for Storybook logo display
- Fix LayoutDecorator structure and preview padding for fullscreen layouts
- Add CLAUDE.storybook.md with Storybook patterns knowledge base

* feat: add 12 family-based stories with full-page-first ordering

Add mockup-based stories for component families to achieve comprehensive
Storybook coverage without complex Redux/context dependencies:

Components:
- AddressBook: Full page, table, dialogs (create/edit/import/remove)
- Batch: Full UI, indicator, sidebar states
- NFTs: Full page, grid, loading/empty, preview modal, collections
- NotificationCenter: Bell with popover, notification variants
- SafeMessages: Full page, message types, signers, signing flow
- Welcome: Welcome screen and onboarding states

Features:
- Bridge: Full page, widget, disabled/in-progress states, history
- NoFeeCampaign: Dashboard banner, gas warning, eligibility states
- Proposers: Full list, chips, edit/delete dialogs
- Speedup: Modal, in-progress/success states, gas slider
- TargetedOutreach: Popup overlay, survey, announcement variants
- TxNotes: History view, input, character limit states

Documentation:
- Add "Story Export Order" section to quickstart.md documenting
  that full-page/comprehensive stories should be exported first
  to appear at the top of Storybook sidebar

* chore: update spec to family and page based stories

* feat: add 7 family-based stories for 100% top-level coverage

Add mockup-based stories with full-page-first pattern for:
- tx-flow: Token transfer, sign message, swap flows
- recovery: Recovery page, status variants, settings
- myAccounts: Account list, pinned safes, multi-chain
- new-safe: Create/load flows, owner configuration
- walletconnect: Connection form, sessions, proposals
- safe-shield: Security analysis, severity levels
- counterfactual: Activation flow, balance states

This completes Phase 10 T121-T128, achieving 100% coverage
of non-skipped top-level component groups.

* chore: update tasks.md with Phase 10 completion status

Mark T121-T128 as complete:
- T121: tx-flow stories
- T122: recovery stories
- T123: myAccounts stories
- T124: new-safe stories
- T125: walletconnect stories
- T127: safe-shield stories
- T128: counterfactual stories

Update coverage status:
- 36/41 groups covered (88%)
- 5 groups intentionally skipped
- 100% coverage of non-skipped groups achieved

* feat: add scrollable all-states stories with docs-style wrappers

Add AllStates/AllSteps stories as first export in each multi-step feature:
- NewSafe: CreateSafeAllSteps (4 steps)
- TxFlow: TokenTransferAllSteps (4 steps)
- WalletConnect: WalletConnectAllStates (6 states)
- Counterfactual: ActivationAllStates (6 states)
- SafeShield: SafeShieldAllStates (6 states)
- Recovery: RecoveryAllStates (5 states)
- MyAccounts: MyAccountsAllStates (6 states)

Each state wrapped with StateWrapper showing:
- State/step name heading
- Description of the state
- Grey background container with full UI

* tests: update story snapshots after all-states story additions

Update snapshots affected by new scrollable all-states stories:
- AssetsTable: story ordering changed
- DateTime: snapshot format update
- TxStatusChip: removed obsolete story snapshots
- HnSignupIntro: new snapshot
- SwapOrderConfirmationView: snapshot update

* chore: add storybook coverage generation script

* chore: update storybook coverage scripts and documentation

* chore: add tests for storybook scripts

* chore: deterministic output of storybook coverage by order and prettier

* chore: use emotion cache in storybook to align design themes of app and storybook

* chore: add full layout to dashboard story

* chore: tx queue to PendingTxsList

* chore: move story mocking to separate file

* chore: use createMockStory on bigger stories

* chore: use createMockStory in more story files

* chores: enable portfolio and positions feature on all dashboard stories

* chore(storybook): use default feature flags in all stories

- Remove explicit features: { portfolio: false, positions: false } overrides
- All stories now use DEFAULT_FEATURES (portfolio, positions, swaps enabled)
- Update Dashboard stories to use wallet: 'owner' for swap button visibility
- Update documentation to clarify feature flag defaults

* chore(storybook): add page stories and improve mock infrastructure

- Add stories for all pages in src/stories/pages/ directory
- Add spaces feature support with auth state and API mocks
- Add useIsOfficialHost mock for Storybook
- Add query parameter support to createMockStory
- Document decorator stacking behavior in AGENTS.md
- Remove Dashboard.stories.tsx (duplicate of home)
- Include .storybook in tsconfig.json for ESLint

* chore(storybook): add Chromatic visual regression testing

- Add GitHub Actions workflow for Chromatic on PRs and pushes to dev/main
- Configure Chromatic with TurboSnap for cost optimization
- Add light/dark theme modes for visual testing
- Fix Storybook build by mocking next/image loader that caused
  "unsupported file type" errors with static image imports

* chore(storybook): enable junit report for Chromatic

* chore(storybook): add chromatic package and local run script

* fix(storybook): remove incompatible junitReport option from chromatic config

junitReport and exitOnceUploaded are incompatible options in Chromatic CLI.

* chore(storybook): auto-accept chromatic changes on dev and main

* fix(storybook): exclude SVGs from asset/resource rule for Chromatic

SVGs were being treated as asset URLs instead of React components,
causing createElement errors in Chromatic. Now SVGs are excluded
so they go through SVGR loader instead.

* chore(storybook): set chromatic viewport to 1280px for full-page layouts

* fix(storybook): deep merge safeInfo.data in story overrides

Allows partial overrides like `data: { deployed: false }` to work
correctly by spreading the base safeInfo data instead of replacing it.

* docs: add Chromatic visual regression testing section (#7081)

* docs: add Chromatic visual regression testing section

* chore: trigger chromatic workflow

* chore: trigger chromatic

* fix(storybook): remove viewports from chromatic config

Chromatic does not support viewports and modes on the same story.
Remove viewports config and rely on modes only for light/dark theme testing.

Also fix StoreOverrides type for safeInfo.data property access.

* fix(storybook): add emotion insertion point for correct CSS ordering

Add preview-head.html with emotion-insertion-point meta tag to ensure
MUI styles are loaded first in production builds. This fixes sidebar
styling issues in Chromatic snapshots.

Also update Home story to use safeTokenHolder scenario (smaller balance)
and add chromatic viewport modes.

* chore(storybook): optimize Chromatic snapshots

- Add theme toolbar switcher with light/dark mode toggle
- Disable Chromatic snapshots for feature documentation stories
- Add !chromatic tags to high-variance story variants
- Reduces redundant snapshots while maintaining visual coverage

* chore: fix prettier formatting in chromatic.config.json

---------

* chore(storybook): add Vite-based Storybook configuration

- Add .storybook-vite/ with Vite-specific config (main.ts, preview.tsx)
- Add custom Vite plugin for @/public/* imports (SVGs via SVGR, images as next/image-compatible objects)
- Add querystring polyfill mock for browser compatibility
- Rename storybook:full to storybook:lazy and make lazy compilation opt-in
- New commands: storybook (normal), storybook:lazy (with lazy compilation), storybook:vite (Vite build)
- Update tsconfig.json to include .storybook-vite files

* chore(chromatic): limit snapshots to 3 core page stories

Only snapshot Home, Balances, and Transactions/History pages
to reduce Chromatic usage during development.

* fix(storybook): add transaction handlers for queue and history pages

Add MSW handlers for transaction-related API endpoints:
- Transaction history list handler
- Transaction details handler with txData: null to avoid parsing errors
- Updated createMockStory to include transaction handlers

The txData is set to null because the Receipt component's data parsing
logic throws errors with mock transaction data structures.

* chore(chromatic): snapshot transaction queue instead of history

Update Chromatic config to capture the Queue page rather than History,
as it better demonstrates the transaction handling with pending items.

* docs: document transaction mocking limitations in AGENTS.md

Add a "Known Limitation" section noting that transaction page stories
have basic MSW handlers but txData mocking is incomplete. Expanding
transaction details may show errors or incomplete data. Further work
is needed to match the CGW response format expected by UI components.

* chore: remove unused storybook script references

Remove storybook:inventory, storybook:coverage, and storybook:dependencies
scripts that reference non-existent files. Only storybook:generate-coverage
is needed and points to the actual generate-storybook-coverage.ts file.

* fix(chromatic): remove onlyChanged flag conflicting with onlyStoryNames

Chromatic CLI doesn't allow both --only-changed and --only-story-names
to be used together. Since we want to limit snapshots to specific stories
to save credits, remove the onlyChanged option.

* tests(storybook): update snapshots for MUI class name changes

Update 196 snapshots to reflect MUI CSS class name changes
from css-* to mui-style-* prefix format.

* fix(chromatic): remove onlyChanged from workflow to use config file

The Chromatic action was passing onlyChanged: true which conflicts with
onlyStoryNames in chromatic.config.json. Remove from workflow since
config file settings take precedence.

* refactor(scripts): reduce cyclomatic complexity in storybook utilities

- Extract lookup tables (PACKAGE_DETECTORS, API_PATTERNS, CATEGORY_PATTERNS, EXPORT_EXTRACTORS) to replace conditional chains
- Add helper functions for isolated concerns (isExternalPackage, detectPackageTypes, categorizeImport, etc.)
- Use switch statements for type discrimination in formatCoverageSource
- Add Code Complexity Guidelines section to AGENTS.md

* refactor(scripts): extract helpers to reduce complexity in storybook utilities

- family.ts: Extract resolveStoryPath and extractStoryExportNames helpers
- generate-storybook-coverage.ts: Extract buildGroupInfoMap and determineCoverageType helpers
- fetch-fixtures.ts: Extract parseSafeArg, fetchSingleSafe, fetchAllFixtures helpers
- chromatic.config.json: Remove invalid externals option causing CI failure

* refactor(scripts): extract generateTopLevelSection helpers to fix CodeScene

Extract formatCoveredGroupRow, formatUncoveredGroupRow, formatSkippedGroupRow,
generateUncoveredGroupsSection, and generateSkippedGroupsSection helpers
to eliminate nested conditional logic in generateTopLevelSection.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-02-04 11:52:24 +01:00

19 KiB

Quick Start: Creating Storybook Stories

Branch: 001-shadcn-storybook-migration | Date: 2026-01-29

This guide walks you through creating Storybook stories for the Safe{Wallet} design system migration.

Table of Contents

  1. Prerequisites
  2. Running Storybook
  3. Component Inventory Tool
  4. Creating a UI Component Story
  5. Creating a Data-Dependent Story
  6. Creating a Page-Level Story
  7. Adding MSW Handlers
  8. Chromatic Review Process
  9. Troubleshooting

Prerequisites

Ensure you have the monorepo set up:

# Install dependencies
yarn install

# Verify Storybook works
yarn workspace @safe-global/web storybook

Running Storybook

# Development mode (with hot reload)
yarn workspace @safe-global/web storybook
# Opens at http://localhost:6006

# Build static version
yarn workspace @safe-global/web build-storybook

# Run snapshot tests
yarn workspace @safe-global/web test:storybook

Component Inventory Tool

The inventory tool helps you find components that need stories and prioritizes them by importance.

Running the Inventory

# Family-based inventory (default)
yarn workspace @safe-global/web storybook:inventory

# Verbose output with details
yarn workspace @safe-global/web storybook:inventory --verbose

# JSON output for processing
yarn workspace @safe-global/web storybook:inventory --json

# Save to file
yarn workspace @safe-global/web storybook:inventory --json --output inventory.json

# Legacy per-component view (opt-in)
yarn workspace @safe-global/web storybook:inventory --components
yarn workspace @safe-global/web storybook:inventory --components --verbose

Example Output

📦 Component Inventory Scanner
==============================

📊 Coverage Summary
-------------------
Total Components: 330
With Stories: 14
Coverage: 4%

📁 Coverage by Category
-----------------------
other        [█░░░░░░░░░░░░░░░░░░░] 9/227 (4%)
transaction  [░░░░░░░░░░░░░░░░░░░░] 0/38 (0%)
sidebar      [░░░░░░░░░░░░░░░░░░░░] 0/3 (0%)

🎯 Top Priority Components (need stories)
------------------------------------------
  SafeHeaderInfo           [sidebar] Score: 20
  MultiAccountContextMenu  [sidebar] Score: 20
  QrModal                  [sidebar] Score: 15

Generating Coverage Reports

# Markdown report
yarn workspace @safe-global/web storybook:coverage --format md --output coverage.md

# HTML report (visual dashboard)
yarn workspace @safe-global/web storybook:coverage --format html --output coverage.html

# JSON report (for CI/tooling)
yarn workspace @safe-global/web storybook:coverage --format json --output coverage.json

Coverage Documentation (Version-Controlled)

The repository maintains a persistent apps/web/.storybook/COVERAGE.md file that tracks coverage over time:

# Regenerate after adding stories
yarn workspace @safe-global/web storybook:generate-coverage

The COVERAGE.md file contains three views:

  1. Top-Level Groups (41 groups): High-level overview where each group can be covered by ONE story file
  2. Family Coverage (560 families): Mid-level view with components grouped by directory
  3. Component Coverage (810 components): Detailed view of every component with story status

This file is checked into the repository, enabling:

  • Historical tracking of coverage changes via git history
  • Quick browsing on GitHub without running tools
  • PR reviews to verify new stories were added

Priority Scoring

Components are scored based on:

  • Category weight: UI (10), Sidebar (15), Common (8)
  • Dependents: Components used by many others score higher
  • Complexity: Simple components (no mocking needed) score higher for quick wins
  • MSW needs: Components that need API mocking get slight boost (common use case)

Work Order

The tool suggests a phased approach:

  1. Phase 1: UI primitives (no dependencies)
  2. Phase 2: Sidebar components (critical for page stories)
  3. Phase 3: Simple common components
  4. Phase 4: Redux-dependent components
  5. Phase 5: MSW-dependent components
  6. Phase 6: Complex components (Web3, multiple decorators)

Family-Based Story Strategy

The family-based approach groups related components by directory and covers them with a single story file containing multiple exports.

Why Family-Based Coverage?

  • Cleaner Sidebar: ~50 organized groups instead of 330+ flat entries
  • Better Chromatic Coverage: Each story export = 1 Chromatic snapshot
  • Contextual Testing: Mini-components (skeletons, empty states) are tested in their parent's context
  • Maintainable: One story file per family, easier to keep updated

Family Coverage Report

# Default mode - shows family-based coverage
yarn workspace @safe-global/web storybook:inventory

Example output:

📊 Family Coverage Summary
--------------------------
Total Families: 178
Covered Families: 22
Complete Families: 17
Family Coverage: 12%
Total Story Exports: 62

📁 Family Coverage by Category
-------------------------------
sidebar      [████████████████████] 3/3 families (19 exports)
dashboard    [███████████░░░░░░░░░] 4/7 families (15 exports)
common       [█████░░░░░░░░░░░░░░░] 3/11 families (8 exports)

Coverage Status Definitions

Status Meaning
complete Has stories with enough exports to cover main states
partial Has some stories but needs more exports for full coverage
none No story file exists for this family

Creating Family Stories

A family story file should cover multiple related components and states:

// PendingTxsList.stories.tsx - covers entire PendingTxs family
// Family components: PendingTxsList, PendingTxListItem, PendingRecoveryListItem, PendingTxsSkeleton

const meta = {
  title: 'Dashboard/PendingTxsList', // ONE sidebar entry
  component: PendingTxsList,
}

// Multiple exports = multiple Chromatic snapshots
export const EmptyQueue: Story = {} // Tests EmptyState child
export const SingleTransaction: Story = {} // Tests PendingTxListItem
export const MultipleTransactions: Story = {} // Tests list rendering
export const ReadyToExecute: Story = {} // Tests confirmation state
export const Loading: Story = {} // Tests PendingTxsSkeleton
export const NonOwnerView: Story = {} // Tests non-owner context

Story Export Order (Sidebar Ordering)

IMPORTANT: The order of story exports determines their order in the Storybook sidebar. Always export comprehensive/full-page stories FIRST, followed by granular component stories.

Why? Users browsing Storybook should see the most complete view of a feature at the top, then can drill down into individual component states below.

Pattern:

// ✅ CORRECT: Full page first, then granular stories
export const FullPage: Story = {} // 1st - Complete view with all components
export const Table: Story = {} // 2nd - Main data display
export const EmptyState: Story = {} // 3rd - Empty data state
export const AddDialog: Story = {} // 4th - Individual dialog
export const EditDialog: Story = {} // 5th - Individual dialog
export const DeleteDialog: Story = {} // 6th - Individual dialog

// ❌ WRONG: Granular stories before full page
export const AddDialog: Story = {} // Dialog shown first - confusing
export const Table: Story = {}
export const FullPage: Story = {} // Full page buried at bottom

Naming conventions for top-level stories:

  • FullPage - Complete page with all sections
  • Overview - Summary view with multiple widgets
  • Default - Standard view (use when there is no clear "full page" variant)

This ensures that when someone opens a component family in Storybook, they immediately see the most comprehensive representation first.

When to Create Separate Story Files vs Add Exports

Add exports to existing family story when:

  • Component is only used within the family
  • Component renders inline (skeleton, empty state)
  • Adding a new state variant

Create new story file when:

  • Component is reused across multiple families
  • Component has its own complex state logic
  • Component is a standalone UI primitive

Creating a UI Component Story

For shadcn/ui primitives (/components/ui/):

Step 1: Create the story file

Story files are colocated with components. Create component-name.stories.tsx next to the component:

src/components/ui/
├── switch.tsx
└── switch.stories.tsx    ← Create this file

Step 2: Use the UI template

import type { Meta, StoryObj } from '@storybook/react'
import { Switch } from './switch'

const meta = {
  title: 'UI/Switch',
  component: Switch,
  tags: ['autodocs'],
  argTypes: {
    checked: { control: 'boolean' },
    disabled: { control: 'boolean' },
    size: {
      control: 'select',
      options: ['sm', 'default', 'lg'],
    },
  },
} satisfies Meta<typeof Switch>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
  args: {
    checked: false,
  },
}

export const Checked: Story = {
  args: {
    checked: true,
  },
}

export const Disabled: Story = {
  args: {
    disabled: true,
  },
}

export const AllVariants: Story = {
  render: () => (
    <div className="flex flex-col gap-4">
      <div className="flex items-center gap-4">
        <Switch />
        <span>Unchecked</span>
      </div>
      <div className="flex items-center gap-4">
        <Switch checked />
        <span>Checked</span>
      </div>
      <div className="flex items-center gap-4">
        <Switch disabled />
        <span>Disabled</span>
      </div>
    </div>
  ),
}

Step 3: Verify in Storybook

  1. Run Storybook: yarn workspace @safe-global/web storybook
  2. Navigate to UI → Switch in the sidebar
  3. Verify all stories render correctly
  4. Check the Docs tab for auto-generated documentation

Creating a Data-Dependent Story

For components that need API data or Redux state:

Step 1: Identify dependencies

Before writing the story, check what the component needs:

// Look for these patterns in the component:
import { useSelector } from 'react-redux' // → Need StoreDecorator
import { useSafeInfo } from '@/hooks/useSafeInfo' // → Need MSW handler
import { useRouter } from 'next/router' // → Need RouterDecorator (usually automatic)

Step 2: Create mock data

Use deterministic values (no faker/random) for consistent snapshots:

// At the top of your story file
const MOCK_ADDRESS = '0x1234567890123456789012345678901234567890'
const MOCK_CHAIN_ID = '1'

const mockApiResponse = {
  address: { value: MOCK_ADDRESS },
  threshold: 2,
  owners: [
    { value: '0xowner1111111111111111111111111111111111' },
    { value: '0xowner2222222222222222222222222222222222' },
  ],
}

Step 3: Add decorators and MSW handlers

import type { Meta, StoryObj } from '@storybook/react'
import { Paper } from '@mui/material'
import { http, HttpResponse } from 'msw'
import { StoreDecorator } from '@/stories/storeDecorator'
import { AddressInfo } from './AddressInfo'

const meta = {
  title: 'Common/AddressInfo',
  component: AddressInfo,
  tags: ['autodocs'],
  parameters: {
    msw: {
      handlers: [
        http.get('*/v1/chains/:chainId/safes/:address', () => {
          return HttpResponse.json(mockApiResponse)
        }),
      ],
    },
  },
  decorators: [
    (Story) => (
      <StoreDecorator initialState={{}}>
        <Paper sx={{ padding: 2 }}>
          <Story />
        </Paper>
      </StoreDecorator>
    ),
  ],
} satisfies Meta<typeof AddressInfo>

Step 4: Add state stories

Always include Loading, Error, and Empty states:

export const Loading: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('*/v1/chains/:chainId/safes/:address', async () => {
          await new Promise((r) => setTimeout(r, 100000))
          return HttpResponse.json({})
        }),
      ],
    },
  },
}

export const Error: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('*/v1/chains/:chainId/safes/:address', () => {
          return HttpResponse.json({ error: 'Failed' }, { status: 500 })
        }),
      ],
    },
  },
}

Creating a Page-Level Story

For full-page stories with sidebar and header:

Step 1: Use fullscreen layout and withLayout decorator

import { withLayout } from '../../../.storybook/decorators'

const meta = {
  title: 'Pages/Dashboard',
  component: MockDashboardPage,
  parameters: {
    layout: 'fullscreen', // Important!
  },
  decorators: [
    // Add store decorator for Redux state
    (Story, context) => (
      <StoreDecorator initialState={{...}}>
        <Story />
      </StoreDecorator>
    ),
    // Add layout decorator for sidebar + header
    withLayout({
      activeNav: 'home',
      safeAddress: '0x9fC3...213e',
      safeBalance: '$4,500,000',
      chainName: 'Ethereum',
    }),
  ],
}

Step 2: withLayout options

The withLayout decorator provides a mock sidebar and header:

withLayout({
  showSidebar?: boolean,     // Show/hide sidebar (default: true)
  showHeader?: boolean,      // Show/hide header (default: true)
  activeNav?: string,        // Highlight nav item: 'home' | 'assets' | 'transactions' | 'apps' | 'settings'
  safeAddress?: string,      // Display address in sidebar
  safeBalance?: string,      // Display balance in sidebar
  chainName?: string,        // Display chain name
})

Step 3: Add viewport stories

Use the pre-configured Safe{Wallet} viewports:

export const Desktop: Story = {
  // Default viewport (1280x800)
}

export const Tablet: Story = {
  parameters: {
    viewport: { defaultViewport: 'tablet' }, // 768x1024
  },
}

export const Mobile: Story = {
  parameters: {
    viewport: { defaultViewport: 'mobile' }, // 375x667
  },
  decorators: [
    // Often hide sidebar on mobile
    withLayout({
      activeNav: 'home',
      showSidebar: false,
    }),
  ],
}

Available Viewports

The following viewports are pre-configured in Storybook:

Viewport Dimensions Use Case
mobile 375x667 iPhone SE/small phones
tablet 768x1024 iPad/tablets
desktop 1280x800 Standard desktop
desktopWide 1920x1080 Wide desktop monitors

Example: Complete Page Story

See src/components/dashboard/Dashboard.stories.tsx for a full example with:

  • MSW handlers for realistic data
  • StoreDecorator for Redux state
  • withLayout for sidebar/header
  • Mobile and tablet viewport variants

Adding MSW Handlers

Use pre-configured fixtures with real API data from staging CGW:

import { fixtureHandlers, FIXTURE_SCENARIOS } from '@safe-global/test/msw/handlers'

const GATEWAY_URL = 'https://safe-client.safe.global'

// In your story:
export const Default: Story = {
  parameters: {
    msw: {
      handlers: fixtureHandlers.efSafe(GATEWAY_URL),
    },
  },
}

// Available scenarios:
// - efSafe: $142M DeFi positions, 8 protocols (best for testing positions)
// - vitalik: 1551 tokens, whale scenario (best for performance testing)
// - spamTokens: Spam token testing
// - safeTokenHolder: 15 diverse DeFi protocols
// - empty: Empty state testing
// - withoutPositions: POSITIONS feature flag disabled

Option 2: Inline handlers (simple overrides)

For component-specific mocking or overriding fixture data:

import { http, HttpResponse } from 'msw'

parameters: {
  msw: {
    handlers: [
      http.get('*/v1/endpoint', () => {
        return HttpResponse.json(mockData)
      }),
    ],
  },
},

Option 3: Combining fixtures with overrides

Layer inline handlers on top of fixtures:

import { fixtureHandlers } from '@safe-global/test/msw/handlers'
import { http, HttpResponse } from 'msw'

export const WithError: Story = {
  parameters: {
    msw: {
      handlers: [
        ...fixtureHandlers.efSafe(GATEWAY_URL),
        // Override specific endpoint with error
        http.get('*/v1/chains/:chainId/safes/:address/balances/*', () => {
          return HttpResponse.json({ error: 'Failed' }, { status: 500 })
        }),
      ],
    },
  },
}

Refreshing fixtures

To update fixtures with fresh data from staging CGW:

npx tsx config/test/msw/scripts/fetch-fixtures.ts

Chromatic Review Process

When PRs have visual changes:

  1. Chromatic runs automatically on PR creation/update
  2. Check the Chromatic status in GitHub PR checks
  3. Click "View in Chromatic" to see visual diffs
  4. Review changes:
    • Green: No visual changes
    • Yellow: Visual changes detected (need approval)
    • Red: Build failed

Approving changes:

  1. Open Chromatic review link
  2. Compare side-by-side diffs
  3. Click Accept for intentional changes
  4. Click Deny for unintended changes → Fix in code
  5. Designer must approve visual changes before merge

Opting out of visual testing:

For stories that shouldn't be visually tested (e.g., loading spinners):

export const LoadingSpinner: Story = {
  parameters: {
    chromatic: { disableSnapshot: true },
  },
}

Troubleshooting

Story doesn't render

  1. Check browser console for errors
  2. Verify all imports are correct
  3. Check if decorators are needed (StoreDecorator, Paper wrapper)

MSW handlers not working

  1. Verify URL pattern matches (use */v1/... for flexible matching)
  2. Check handler is in parameters.msw.handlers array
  3. Use browser Network tab to see actual request URLs

TypeScript errors

# Run type-check to see all errors
yarn workspace @safe-global/web type-check

Snapshots failing

# Update snapshots after intentional changes
yarn workspace @safe-global/web test:storybook -u

Component needs router

Storybook's Next.js integration handles router automatically. If issues persist:

import { RouterDecorator } from '@/stories/routerDecorator'

decorators: [
  (Story) => (
    <RouterDecorator router={{ pathname: '/dashboard' }}>
      <Story />
    </RouterDecorator>
  ),
],

Checklist: Before Committing

  • Story file created next to component
  • tags: ['autodocs'] included
  • All significant states covered (default, loading, error, empty, disabled)
  • Deterministic mock data (no random/faker values)
  • Type-check passes: yarn workspace @safe-global/web type-check
  • Stories render correctly in Storybook
  • No console errors

Templates

Full templates are available in specs/001-shadcn-storybook-migration/contracts/:

  • ui-component.stories.template.tsx - For shadcn primitives
  • common-component.stories.template.tsx - For data-dependent components
  • page.stories.template.tsx - For full-page layouts
  • msw-handler.template.ts - For organized MSW handlers