Skip to content

Commit 9849aab

Browse files
authored
test(#10336): add components/MkC.* stories (#13830)
* test(storybook): add `components/MkC.*` stories * test(storybook): add some tests * test: add sleep * test: comment-out flaky test * test(storybook): add test for `MkChannelFollowButton` * chore(storybook): tweak sleep duration in `MkChannelFollowButton` story test * fix(chromatic): add delay to `MkChannelList` * chore: replace `mswDecorator` with `mswLoader` * fix(storybook): tweak some parameters * chore: serve static files * fix(chromatic): add delay to `MkCwButton` * chore: delete logging for debug * fix: add right click in `MkContextMenu` play * refactor: remove unused imports
1 parent 61fae45 commit 9849aab

28 files changed

+1083
-86
lines changed

packages/frontend/.storybook/fakes.ts

+60
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,66 @@ export function abuseUserReport() {
2222
};
2323
}
2424

25+
export function channel(id = 'somechannelid', name = 'Some Channel', bannerUrl: string | null = 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true'): entities.Channel {
26+
return {
27+
id,
28+
createdAt: '2016-12-28T22:49:51.000Z',
29+
lastNotedAt: '2016-12-28T22:49:51.000Z',
30+
name,
31+
description: null,
32+
userId: null,
33+
bannerUrl,
34+
pinnedNoteIds: [],
35+
color: '#000',
36+
isArchived: false,
37+
usersCount: 1,
38+
notesCount: 1,
39+
isSensitive: false,
40+
allowRenoteToExternal: false,
41+
};
42+
}
43+
44+
export function clip(id = 'someclipid', name = 'Some Clip'): entities.Clip {
45+
return {
46+
id,
47+
createdAt: '2016-12-28T22:49:51.000Z',
48+
lastClippedAt: null,
49+
userId: 'someuserid',
50+
user: {
51+
id: 'someuserid',
52+
name: 'Misskey User',
53+
username: 'miskist',
54+
host: 'misskey-hub.net',
55+
avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true',
56+
avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
57+
avatarDecorations: [],
58+
emojis: {},
59+
badgeRoles: [],
60+
onlineStatus: 'unknown',
61+
},
62+
notesCount: undefined,
63+
name,
64+
description: 'Some clip description',
65+
isPublic: false,
66+
favoritedCount: 0,
67+
};
68+
}
69+
70+
export function emojiDetailed(id = 'someemojiid', name = 'some_emoji'): entities.EmojiDetailed {
71+
return {
72+
id,
73+
aliases: ['alias1', 'alias2'],
74+
name,
75+
category: 'emojiCategory',
76+
host: null,
77+
url: '/client-assets/about-icon.png',
78+
license: null,
79+
isSensitive: false,
80+
localOnly: false,
81+
roleIdsThatCanBeUsedThisEmojiAsReaction: ['roleId1', 'roleId2'],
82+
};
83+
}
84+
2585
export function galleryPost(isSensitive = false) {
2686
return {
2787
id: 'somepostid',

packages/frontend/.storybook/generate.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ function toStories(component: string): Promise<string> {
397397
const globs = await Promise.all([
398398
glob('src/components/global/Mk*.vue'),
399399
glob('src/components/global/RouterView.vue'),
400-
glob('src/components/Mk{A,B}*.vue'),
400+
glob('src/components/Mk[A-C]*.vue'),
401401
glob('src/components/MkDigitalClock.vue'),
402402
glob('src/components/MkGalleryPostPreview.vue'),
403403
glob('src/components/MkSignupServerRules.vue'),

packages/frontend/.storybook/main.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const _dirname = fileURLToPath(new URL('.', import.meta.url));
1515

1616
const config = {
1717
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
18+
staticDirs: [{ from: '../assets', to: '/client-assets' }],
1819
addons: [
1920
getAbsolutePath('@storybook/addon-essentials'),
2021
getAbsolutePath('@storybook/addon-interactions'),

packages/frontend/.storybook/preview.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { FORCE_REMOUNT } from '@storybook/core-events';
77
import { addons } from '@storybook/preview-api';
88
import { type Preview, setup } from '@storybook/vue3';
99
import isChromatic from 'chromatic/isChromatic';
10-
import { initialize, mswDecorator } from 'msw-storybook-addon';
10+
import { initialize, mswLoader } from 'msw-storybook-addon';
1111
import { userDetailed } from './fakes.js';
1212
import locale from './locale.js';
1313
import { commonHandlers, onUnhandledRequest } from './mocks.js';
@@ -122,7 +122,6 @@ const preview = {
122122
}
123123
return story;
124124
},
125-
mswDecorator,
126125
(Story, context) => {
127126
return {
128127
setup() {
@@ -137,6 +136,7 @@ const preview = {
137136
};
138137
},
139138
],
139+
loaders: [mswLoader],
140140
parameters: {
141141
controls: {
142142
exclude: /^__/,

packages/frontend/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"@types/node": "20.12.7",
105105
"@types/punycode": "2.1.4",
106106
"@types/sanitize-html": "2.11.0",
107+
"@types/seedrandom": "3.0.8",
107108
"@types/throttle-debounce": "5.0.2",
108109
"@types/tinycolor2": "1.4.6",
109110
"@types/uuid": "9.0.8",
@@ -128,6 +129,7 @@
128129
"prettier": "3.2.5",
129130
"react": "18.3.1",
130131
"react-dom": "18.3.1",
132+
"seedrandom": "3.0.5",
131133
"start-server-and-test": "2.0.3",
132134
"storybook": "8.0.9",
133135
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and misskey-project
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
7+
/* eslint-disable import/no-default-export */
8+
import { StoryObj } from '@storybook/vue3';
9+
import { HttpResponse, http } from 'msw';
10+
import { action } from '@storybook/addon-actions';
11+
import { expect, userEvent, within } from '@storybook/test';
12+
import { channel } from '../../.storybook/fakes.js';
13+
import { commonHandlers } from '../../.storybook/mocks.js';
14+
import MkChannelFollowButton from './MkChannelFollowButton.vue';
15+
import { semaphore } from '@/scripts/test-utils.js';
16+
import { i18n } from '@/i18n.js';
17+
18+
function sleep(ms: number) {
19+
return new Promise(resolve => setTimeout(resolve, ms));
20+
}
21+
22+
const s = semaphore();
23+
export const Default = {
24+
render(args) {
25+
return {
26+
components: {
27+
MkChannelFollowButton,
28+
},
29+
setup() {
30+
return {
31+
args,
32+
};
33+
},
34+
computed: {
35+
props() {
36+
return {
37+
...this.args,
38+
};
39+
},
40+
},
41+
template: '<MkChannelFollowButton v-bind="props" />',
42+
};
43+
},
44+
args: {
45+
channel: channel(),
46+
full: true,
47+
},
48+
async play({ canvasElement }) {
49+
await s.acquire();
50+
await sleep(1000);
51+
const canvas = within(canvasElement);
52+
const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
53+
await expect(buttonElement).toHaveTextContent(i18n.ts.follow);
54+
await userEvent.click(buttonElement);
55+
await sleep(1000);
56+
await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow);
57+
await sleep(100);
58+
await userEvent.click(buttonElement);
59+
s.release();
60+
},
61+
parameters: {
62+
layout: 'centered',
63+
msw: {
64+
handlers: [
65+
...commonHandlers,
66+
http.post('/api/channels/follow', async ({ request }) => {
67+
action('POST /api/channels/follow')(await request.json());
68+
return HttpResponse.json({});
69+
}),
70+
http.post('/api/channels/unfollow', async ({ request }) => {
71+
action('POST /api/channels/unfollow')(await request.json());
72+
return HttpResponse.json({});
73+
}),
74+
],
75+
},
76+
},
77+
} satisfies StoryObj<typeof MkChannelFollowButton>;

packages/frontend/src/components/MkChannelFollowButton.vue

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,18 @@ SPDX-License-Identifier: AGPL-3.0-only
2626

2727
<script lang="ts" setup>
2828
import { ref } from 'vue';
29+
import * as Misskey from 'misskey-js';
2930
import { misskeyApi } from '@/scripts/misskey-api.js';
3031
import { i18n } from '@/i18n.js';
3132

3233
const props = withDefaults(defineProps<{
33-
channel: Record<string, any>;
34+
channel: Misskey.entities.Channel;
3435
full?: boolean;
3536
}>(), {
3637
full: false,
3738
});
3839

39-
const isFollowing = ref<boolean>(props.channel.isFollowing);
40+
const isFollowing = ref(props.channel.isFollowing);
4041
const wait = ref(false);
4142

4243
async function onClick() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and misskey-project
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
7+
/* eslint-disable import/no-default-export */
8+
import { StoryObj } from '@storybook/vue3';
9+
import { HttpResponse, http } from 'msw';
10+
import { action } from '@storybook/addon-actions';
11+
import { channel } from '../../.storybook/fakes.js';
12+
import { commonHandlers } from '../../.storybook/mocks.js';
13+
import MkChannelList from './MkChannelList.vue';
14+
export const Default = {
15+
render(args) {
16+
return {
17+
components: {
18+
MkChannelList,
19+
},
20+
setup() {
21+
return {
22+
args,
23+
};
24+
},
25+
computed: {
26+
props() {
27+
return {
28+
...this.args,
29+
};
30+
},
31+
},
32+
template: '<MkChannelList v-bind="props" />',
33+
};
34+
},
35+
args: {
36+
pagination: {
37+
endpoint: 'channels/search',
38+
limit: 10,
39+
},
40+
},
41+
parameters: {
42+
chromatic: {
43+
// NOTE: ロードが終わるまで待つ
44+
delay: 3000,
45+
},
46+
layout: 'fullscreen',
47+
msw: {
48+
handlers: [
49+
...commonHandlers,
50+
http.post('/api/channels/search', async ({ request, params }) => {
51+
action('POST /api/channels/search')(await request.json());
52+
return HttpResponse.json(params.untilId === 'lastchannel' ? [] : [
53+
channel(),
54+
channel('lastchannel', 'Last Channel', null),
55+
]);
56+
}),
57+
],
58+
},
59+
},
60+
decorators: [
61+
() => ({
62+
template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
63+
}),
64+
],
65+
} satisfies StoryObj<typeof MkChannelList>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and misskey-project
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
7+
/* eslint-disable import/no-default-export */
8+
import { StoryObj } from '@storybook/vue3';
9+
import { channel } from '../../.storybook/fakes.js';
10+
import MkChannelPreview from './MkChannelPreview.vue';
11+
export const Default = {
12+
render(args) {
13+
return {
14+
components: {
15+
MkChannelPreview,
16+
},
17+
setup() {
18+
return {
19+
args,
20+
};
21+
},
22+
computed: {
23+
props() {
24+
return {
25+
...this.args,
26+
};
27+
},
28+
},
29+
template: '<MkChannelPreview v-bind="props" />',
30+
};
31+
},
32+
args: {
33+
channel: channel(),
34+
},
35+
parameters: {
36+
layout: 'fullscreen',
37+
},
38+
decorators: [
39+
() => ({
40+
template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>',
41+
}),
42+
],
43+
} satisfies StoryObj<typeof MkChannelPreview>;

0 commit comments

Comments
 (0)