mirror of
https://github.com/mastodon/mastodon.git
synced 2025-07-15 08:48:15 +00:00
Storybook Helpers (#35158)
This commit is contained in:
parent
0a7418e6d8
commit
c52848b444
|
@ -11,7 +11,21 @@ const config: StorybookConfig = {
|
|||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ['./static'],
|
||||
staticDirs: [
|
||||
'./static',
|
||||
// We need to manually specify the assets because of the symlink in public/sw.js
|
||||
...[
|
||||
'avatars',
|
||||
'emoji',
|
||||
'headers',
|
||||
'sounds',
|
||||
'badge.png',
|
||||
'loading.gif',
|
||||
'loading.png',
|
||||
'oops.gif',
|
||||
'oops.png',
|
||||
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
@ -2,16 +2,19 @@ import { useEffect, useState } from 'react';
|
|||
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import type { Preview } from '@storybook/react-vite';
|
||||
import { http, passthrough } from 'msw';
|
||||
import { initialize, mswLoader } from 'msw-storybook-addon';
|
||||
import { action } from 'storybook/actions';
|
||||
|
||||
import type { LocaleData } from '@/mastodon/locales';
|
||||
import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
|
||||
import { defaultMiddleware } from '@/mastodon/store/store';
|
||||
import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
|
||||
|
||||
// If you want to run the dark theme during development,
|
||||
// you can change the below to `/application.scss`
|
||||
|
@ -22,7 +25,9 @@ const localeFiles = import.meta.glob('@/mastodon/locales/*.json', {
|
|||
});
|
||||
|
||||
// Initialize MSW
|
||||
initialize();
|
||||
initialize({
|
||||
onUnhandledRequest: unhandledRequestHandler,
|
||||
});
|
||||
|
||||
const preview: Preview = {
|
||||
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
|
@ -94,6 +99,21 @@ const preview: Preview = {
|
|||
</IntlProvider>
|
||||
);
|
||||
},
|
||||
(Story) => (
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
<Route
|
||||
path='*'
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
render={({ location }) => {
|
||||
if (location.pathname !== '/') {
|
||||
action(`route change to ${location.pathname}`)(location);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
loaders: [mswLoader],
|
||||
parameters: {
|
||||
|
@ -115,20 +135,10 @@ const preview: Preview = {
|
|||
|
||||
state: {},
|
||||
|
||||
// Force docs to use an iframe as it breaks MSW handlers.
|
||||
// See: https://github.com/mswjs/msw-storybook-addon/issues/83
|
||||
docs: {
|
||||
story: {
|
||||
inline: false,
|
||||
},
|
||||
},
|
||||
docs: {},
|
||||
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get('/index.json', passthrough),
|
||||
http.get('/packs-dev/*', passthrough),
|
||||
http.get('/sounds/*', passthrough),
|
||||
],
|
||||
handlers: mockHandlers,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { render, fireEvent, screen } from 'mastodon/test_helpers';
|
||||
import { render, fireEvent, screen } from '@/testing/rendering';
|
||||
|
||||
import { Button } from '../button';
|
||||
|
||||
|
|
120
app/javascript/mastodon/components/account/account.stories.tsx
Normal file
120
app/javascript/mastodon/components/account/account.stories.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
|
||||
|
||||
import { Account } from './index';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Account',
|
||||
component: Account,
|
||||
argTypes: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the account to display',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
description: 'Size of the avatar in pixels',
|
||||
},
|
||||
hidden: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the account is hidden or not',
|
||||
},
|
||||
minimal: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to display a minimal version of the account',
|
||||
},
|
||||
defaultAction: {
|
||||
type: 'string',
|
||||
control: 'select',
|
||||
options: ['block', 'mute'],
|
||||
description: 'Default action to take on the account',
|
||||
},
|
||||
withBio: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to display the account bio or not',
|
||||
},
|
||||
withMenu: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to display the account menu or not',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
id: '1',
|
||||
size: 46,
|
||||
hidden: false,
|
||||
minimal: false,
|
||||
defaultAction: 'mute',
|
||||
withBio: false,
|
||||
withMenu: true,
|
||||
},
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState(),
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Account>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
id: '1',
|
||||
},
|
||||
};
|
||||
|
||||
export const Hidden: Story = {
|
||||
args: {
|
||||
hidden: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Minimal: Story = {
|
||||
args: {
|
||||
minimal: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithBio: Story = {
|
||||
args: {
|
||||
withBio: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoMenu: Story = {
|
||||
args: {
|
||||
withMenu: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Blocked: Story = {
|
||||
args: {
|
||||
defaultAction: 'block',
|
||||
},
|
||||
parameters: {
|
||||
state: {
|
||||
relationships: {
|
||||
'1': relationshipsFactory({
|
||||
blocking: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Muted: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
state: {
|
||||
relationships: {
|
||||
'1': relationshipsFactory({
|
||||
muting: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import { render, fireEvent, screen } from 'mastodon/test_helpers';
|
||||
import { render, fireEvent, screen } from '@/testing/rendering';
|
||||
|
||||
import Column from '../column';
|
||||
|
||||
|
|
53
app/javascript/testing/api.ts
Normal file
53
app/javascript/testing/api.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { http, HttpResponse } from 'msw';
|
||||
import { action } from 'storybook/actions';
|
||||
|
||||
import { relationshipsFactory } from './factories';
|
||||
|
||||
export const mockHandlers = {
|
||||
mute: http.post<{ id: string }>('/api/v1/accounts/:id/mute', ({ params }) => {
|
||||
action('muting account')(params);
|
||||
return HttpResponse.json(
|
||||
relationshipsFactory({ id: params.id, muting: true }),
|
||||
);
|
||||
}),
|
||||
unmute: http.post<{ id: string }>(
|
||||
'/api/v1/accounts/:id/unmute',
|
||||
({ params }) => {
|
||||
action('unmuting account')(params);
|
||||
return HttpResponse.json(
|
||||
relationshipsFactory({ id: params.id, muting: false }),
|
||||
);
|
||||
},
|
||||
),
|
||||
block: http.post<{ id: string }>(
|
||||
'/api/v1/accounts/:id/block',
|
||||
({ params }) => {
|
||||
action('blocking account')(params);
|
||||
return HttpResponse.json(
|
||||
relationshipsFactory({ id: params.id, blocking: true }),
|
||||
);
|
||||
},
|
||||
),
|
||||
unblock: http.post<{ id: string }>(
|
||||
'/api/v1/accounts/:id/unblock',
|
||||
({ params }) => {
|
||||
action('unblocking account')(params);
|
||||
return HttpResponse.json(
|
||||
relationshipsFactory({
|
||||
id: params.id,
|
||||
blocking: false,
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
export const unhandledRequestHandler = ({ url }: Request) => {
|
||||
const { pathname } = new URL(url);
|
||||
if (pathname.startsWith('/api/v1/')) {
|
||||
action(`unhandled request to ${pathname}`)(url);
|
||||
console.warn(
|
||||
`Unhandled request to ${pathname}. Please add a handler for this request in your storybook configuration.`,
|
||||
);
|
||||
}
|
||||
};
|
70
app/javascript/testing/factories.ts
Normal file
70
app/javascript/testing/factories.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships';
|
||||
import { createAccountFromServerJSON } from '@/mastodon/models/account';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
|
||||
type FactoryOptions<T> = {
|
||||
id?: string;
|
||||
} & Partial<T>;
|
||||
|
||||
type FactoryFunction<T> = (options?: FactoryOptions<T>) => T;
|
||||
|
||||
export const accountFactory: FactoryFunction<ApiAccountJSON> = ({
|
||||
id,
|
||||
...data
|
||||
} = {}) => ({
|
||||
id: id ?? '1',
|
||||
acct: 'testuser',
|
||||
avatar: '/avatars/original/missing.png',
|
||||
avatar_static: '/avatars/original/missing.png',
|
||||
username: 'testuser',
|
||||
display_name: 'Test User',
|
||||
bot: false,
|
||||
created_at: '2023-01-01T00:00:00.000Z',
|
||||
discoverable: true,
|
||||
emojis: [],
|
||||
fields: [],
|
||||
followers_count: 0,
|
||||
following_count: 0,
|
||||
group: false,
|
||||
header: '/header.png',
|
||||
header_static: '/header_static.png',
|
||||
indexable: true,
|
||||
last_status_at: '2023-01-01',
|
||||
locked: false,
|
||||
mute_expires_at: null,
|
||||
note: 'This is a test user account.',
|
||||
statuses_count: 0,
|
||||
suspended: false,
|
||||
url: '/@testuser',
|
||||
uri: '/users/testuser',
|
||||
noindex: false,
|
||||
roles: [],
|
||||
hide_collections: false,
|
||||
...data,
|
||||
});
|
||||
|
||||
export const accountFactoryState = (
|
||||
options: FactoryOptions<ApiAccountJSON> = {},
|
||||
) => createAccountFromServerJSON(accountFactory(options));
|
||||
|
||||
export const relationshipsFactory: FactoryFunction<ApiRelationshipJSON> = ({
|
||||
id,
|
||||
...data
|
||||
} = {}) => ({
|
||||
id: id ?? '1',
|
||||
following: false,
|
||||
followed_by: false,
|
||||
blocking: false,
|
||||
blocked_by: false,
|
||||
languages: null,
|
||||
muting_notifications: false,
|
||||
note: '',
|
||||
requested_by: false,
|
||||
muting: false,
|
||||
requested: false,
|
||||
domain_blocking: false,
|
||||
endorsed: false,
|
||||
notifying: false,
|
||||
showing_reblogs: true,
|
||||
...data,
|
||||
});
|
|
@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router';
|
|||
import type { RenderOptions } from '@testing-library/react';
|
||||
import { render as rtlRender } from '@testing-library/react';
|
||||
|
||||
import { IdentityContext } from './identity_context';
|
||||
import { IdentityContext } from '@/mastodon/identity_context';
|
||||
|
||||
beforeAll(() => {
|
||||
global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {
|
|
@ -251,8 +251,7 @@ export default tseslint.config([
|
|||
devDependencies: [
|
||||
'eslint.config.mjs',
|
||||
'app/javascript/mastodon/performance.js',
|
||||
'app/javascript/mastodon/test_setup.js',
|
||||
'app/javascript/mastodon/test_helpers.tsx',
|
||||
'app/javascript/testing/**/*',
|
||||
'app/javascript/**/__tests__/**',
|
||||
'app/javascript/**/*.stories.ts',
|
||||
'app/javascript/**/*.stories.tsx',
|
||||
|
|
|
@ -29,10 +29,7 @@
|
|||
"vite.config.mts",
|
||||
"vitest.config.mts",
|
||||
"config/vite",
|
||||
"app/javascript/mastodon",
|
||||
"app/javascript/entrypoints",
|
||||
"app/javascript/types",
|
||||
".storybook/*.ts",
|
||||
".storybook/*.tsx"
|
||||
"app/javascript",
|
||||
".storybook/*"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user