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',
|
name: '@storybook/react-vite',
|
||||||
options: {},
|
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;
|
export default config;
|
||||||
|
|
|
@ -2,16 +2,19 @@ import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
|
import { MemoryRouter, Route } from 'react-router';
|
||||||
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import type { Preview } from '@storybook/react-vite';
|
import type { Preview } from '@storybook/react-vite';
|
||||||
import { http, passthrough } from 'msw';
|
|
||||||
import { initialize, mswLoader } from 'msw-storybook-addon';
|
import { initialize, mswLoader } from 'msw-storybook-addon';
|
||||||
|
import { action } from 'storybook/actions';
|
||||||
|
|
||||||
import type { LocaleData } from '@/mastodon/locales';
|
import type { LocaleData } from '@/mastodon/locales';
|
||||||
import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
|
import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
|
||||||
import { defaultMiddleware } from '@/mastodon/store/store';
|
import { defaultMiddleware } from '@/mastodon/store/store';
|
||||||
|
import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
|
||||||
|
|
||||||
// If you want to run the dark theme during development,
|
// If you want to run the dark theme during development,
|
||||||
// you can change the below to `/application.scss`
|
// you can change the below to `/application.scss`
|
||||||
|
@ -22,7 +25,9 @@ const localeFiles = import.meta.glob('@/mastodon/locales/*.json', {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize MSW
|
// Initialize MSW
|
||||||
initialize();
|
initialize({
|
||||||
|
onUnhandledRequest: unhandledRequestHandler,
|
||||||
|
});
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
|
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
|
||||||
|
@ -94,6 +99,21 @@ const preview: Preview = {
|
||||||
</IntlProvider>
|
</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],
|
loaders: [mswLoader],
|
||||||
parameters: {
|
parameters: {
|
||||||
|
@ -115,20 +135,10 @@ const preview: Preview = {
|
||||||
|
|
||||||
state: {},
|
state: {},
|
||||||
|
|
||||||
// Force docs to use an iframe as it breaks MSW handlers.
|
docs: {},
|
||||||
// See: https://github.com/mswjs/msw-storybook-addon/issues/83
|
|
||||||
docs: {
|
|
||||||
story: {
|
|
||||||
inline: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [
|
handlers: mockHandlers,
|
||||||
http.get('/index.json', passthrough),
|
|
||||||
http.get('/packs-dev/*', passthrough),
|
|
||||||
http.get('/sounds/*', passthrough),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import renderer from 'react-test-renderer';
|
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';
|
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';
|
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 type { RenderOptions } from '@testing-library/react';
|
||||||
import { render as rtlRender } from '@testing-library/react';
|
import { render as rtlRender } from '@testing-library/react';
|
||||||
|
|
||||||
import { IdentityContext } from './identity_context';
|
import { IdentityContext } from '@/mastodon/identity_context';
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {
|
global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {
|
|
@ -251,8 +251,7 @@ export default tseslint.config([
|
||||||
devDependencies: [
|
devDependencies: [
|
||||||
'eslint.config.mjs',
|
'eslint.config.mjs',
|
||||||
'app/javascript/mastodon/performance.js',
|
'app/javascript/mastodon/performance.js',
|
||||||
'app/javascript/mastodon/test_setup.js',
|
'app/javascript/testing/**/*',
|
||||||
'app/javascript/mastodon/test_helpers.tsx',
|
|
||||||
'app/javascript/**/__tests__/**',
|
'app/javascript/**/__tests__/**',
|
||||||
'app/javascript/**/*.stories.ts',
|
'app/javascript/**/*.stories.ts',
|
||||||
'app/javascript/**/*.stories.tsx',
|
'app/javascript/**/*.stories.tsx',
|
||||||
|
|
|
@ -29,10 +29,7 @@
|
||||||
"vite.config.mts",
|
"vite.config.mts",
|
||||||
"vitest.config.mts",
|
"vitest.config.mts",
|
||||||
"config/vite",
|
"config/vite",
|
||||||
"app/javascript/mastodon",
|
"app/javascript",
|
||||||
"app/javascript/entrypoints",
|
".storybook/*"
|
||||||
"app/javascript/types",
|
|
||||||
".storybook/*.ts",
|
|
||||||
".storybook/*.tsx"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user