diff options
| author | zyoshoka <107108195+zyoshoka@users.noreply.github.com> | 2024-06-08 18:00:54 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-08 18:00:54 +0900 |
| commit | 9849aab40283cbde2184e74d4795aec8ef8ccba3 (patch) | |
| tree | 913efa935d00b01f9936794e74e410283ba1dbc5 /packages/frontend/src/components | |
| parent | feat: 通報を受けた際にメールまたはWebhookで通知を送出出... (diff) | |
| download | sharkey-9849aab40283cbde2184e74d4795aec8ef8ccba3.tar.gz sharkey-9849aab40283cbde2184e74d4795aec8ef8ccba3.tar.bz2 sharkey-9849aab40283cbde2184e74d4795aec8ef8ccba3.zip | |
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
Diffstat (limited to 'packages/frontend/src/components')
21 files changed, 960 insertions, 42 deletions
diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts new file mode 100644 index 0000000000..b99620da22 --- /dev/null +++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { expect, userEvent, within } from '@storybook/test'; +import { channel } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkChannelFollowButton from './MkChannelFollowButton.vue'; +import { semaphore } from '@/scripts/test-utils.js'; +import { i18n } from '@/i18n.js'; + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +const s = semaphore(); +export const Default = { + render(args) { + return { + components: { + MkChannelFollowButton, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkChannelFollowButton v-bind="props" />', + }; + }, + args: { + channel: channel(), + full: true, + }, + async play({ canvasElement }) { + await s.acquire(); + await sleep(1000); + const canvas = within(canvasElement); + const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); + await expect(buttonElement).toHaveTextContent(i18n.ts.follow); + await userEvent.click(buttonElement); + await sleep(1000); + await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow); + await sleep(100); + await userEvent.click(buttonElement); + s.release(); + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/channels/follow', async ({ request }) => { + action('POST /api/channels/follow')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/channels/unfollow', async ({ request }) => { + action('POST /api/channels/unfollow')(await request.json()); + return HttpResponse.json({}); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkChannelFollowButton>; diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue index 6b1b380e41..841d37a568 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.vue +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -26,17 +26,18 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ - channel: Record<string, any>; + channel: Misskey.entities.Channel; full?: boolean; }>(), { full: false, }); -const isFollowing = ref<boolean>(props.channel.isFollowing); +const isFollowing = ref(props.channel.isFollowing); const wait = ref(false); async function onClick() { diff --git a/packages/frontend/src/components/MkChannelList.stories.impl.ts b/packages/frontend/src/components/MkChannelList.stories.impl.ts new file mode 100644 index 0000000000..f69b20c049 --- /dev/null +++ b/packages/frontend/src/components/MkChannelList.stories.impl.ts @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { channel } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkChannelList from './MkChannelList.vue'; +export const Default = { + render(args) { + return { + components: { + MkChannelList, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkChannelList v-bind="props" />', + }; + }, + args: { + pagination: { + endpoint: 'channels/search', + limit: 10, + }, + }, + parameters: { + chromatic: { + // NOTE: ロードが終わるまで待つ + delay: 3000, + }, + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/channels/search', async ({ request, params }) => { + action('POST /api/channels/search')(await request.json()); + return HttpResponse.json(params.untilId === 'lastchannel' ? [] : [ + channel(), + channel('lastchannel', 'Last Channel', null), + ]); + }), + ], + }, + }, + decorators: [ + () => ({ + 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>', + }), + ], +} satisfies StoryObj<typeof MkChannelList>; diff --git a/packages/frontend/src/components/MkChannelPreview.stories.impl.ts b/packages/frontend/src/components/MkChannelPreview.stories.impl.ts new file mode 100644 index 0000000000..de0193c78f --- /dev/null +++ b/packages/frontend/src/components/MkChannelPreview.stories.impl.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { channel } from '../../.storybook/fakes.js'; +import MkChannelPreview from './MkChannelPreview.vue'; +export const Default = { + render(args) { + return { + components: { + MkChannelPreview, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkChannelPreview v-bind="props" />', + }; + }, + args: { + channel: channel(), + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + 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>', + }), + ], +} satisfies StoryObj<typeof MkChannelPreview>; diff --git a/packages/frontend/src/components/MkChart.stories.impl.ts b/packages/frontend/src/components/MkChart.stories.impl.ts new file mode 100644 index 0000000000..6b0cc3b858 --- /dev/null +++ b/packages/frontend/src/components/MkChart.stories.impl.ts @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { DefaultBodyType, HttpResponse, HttpResponseResolver, JsonBodyType, PathParams, http } from 'msw'; +import seedrandom from 'seedrandom'; +import { action } from '@storybook/addon-actions'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkChart from './MkChart.vue'; + +function getChartArray(seed: string, limit: number, option?: { accumulate?: boolean, mul?: number }): number[] { + const rng = seedrandom(seed); + const max = Math.floor(option?.mul ?? 250 * rng()); + let accumulation = 0; + const array: number[] = []; + for (let i = 0; i < limit; i++) { + const num = Math.floor((max + 1) * rng()); + if (option?.accumulate) { + accumulation += num; + array.unshift(accumulation); + } else { + array.push(num); + } + } + return array; +} + +function getChartResolver(fields: string[], option?: { accumulate?: boolean, mulMap?: Record<string, number> }): HttpResponseResolver<PathParams, DefaultBodyType, JsonBodyType> { + return ({ request }) => { + action(`GET ${request.url}`)(); + const limitParam = new URL(request.url).searchParams.get('limit'); + const limit = limitParam ? parseInt(limitParam) : 30; + const res = {}; + for (const field of fields) { + const layers = field.split('.'); + let current = res; + while (layers.length > 1) { + const currentKey = layers.shift()!; + if (current[currentKey] == null) current[currentKey] = {}; + current = current[currentKey]; + } + current[layers[0]] = getChartArray(field, limit, { + accumulate: option?.accumulate, + mul: option?.mulMap != null && field in option.mulMap ? option.mulMap[field] : undefined, + }); + } + return HttpResponse.json(res); + }; +} + +const Base = { + render(args) { + return { + components: { + MkChart, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkChart v-bind="props" />', + }; + }, + args: { + src: 'federation', + span: 'hour', + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.get('/api/charts/federation', getChartResolver( + ['deliveredInstances', 'inboxInstances', 'stalled', 'sub', 'pub', 'pubsub', 'subActive', 'pubActive'], + )), + http.get('/api/charts/notes', getChartResolver( + ['local.total', 'remote.total'], + { accumulate: true }, + )), + http.get('/api/charts/drive', getChartResolver( + ['local.incSize', 'local.decSize', 'remote.incSize', 'remote.decSize'], + { mulMap: { 'local.incSize': 1e7, 'local.decSize': 5e6, 'remote.incSize': 1e6, 'remote.decSize': 5e5 } }, + )), + ], + }, + }, +} satisfies StoryObj<typeof MkChart>; +export const FederationChart = { + ...Base, + args: { + src: 'federation', + }, +} satisfies StoryObj<typeof MkChart>; +export const NotesTotalChart = { + ...Base, + args: { + src: 'notes-total', + }, +} satisfies StoryObj<typeof MkChart>; +export const DriveChart = { + ...Base, + args: { + src: 'drive', + }, +} satisfies StoryObj<typeof MkChart>; diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 04b6d2f29c..a823a0ec4b 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -19,8 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only id-denylist violation when setting it. This is causing about 60+ lint issues. As this is part of Chart.js's API it makes sense to disable the check here. */ -import { onMounted, ref, shallowRef, watch, PropType } from 'vue'; +import { onMounted, ref, shallowRef, watch } from 'vue'; import { Chart } from 'chart.js'; +import * as Misskey from 'misskey-js'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; @@ -34,44 +35,55 @@ import MkChartLegend from '@/components/MkChartLegend.vue'; initChart(); -const props = defineProps({ - src: { - type: String, - required: true, - }, - args: { - type: Object, - required: false, - }, - limit: { - type: Number, - required: false, - default: 90, - }, - span: { - type: String as PropType<'hour' | 'day'>, - required: true, - }, - detailed: { - type: Boolean, - required: false, - default: false, - }, - stacked: { - type: Boolean, - required: false, - default: false, - }, - bar: { - type: Boolean, - required: false, - default: false, - }, - aspectRatio: { - type: Number, - required: false, - default: null, - }, +type ChartSrc = + | 'federation' + | 'ap-request' + | 'users' + | 'users-total' + | 'active-users' + | 'notes' + | 'local-notes' + | 'remote-notes' + | 'notes-total' + | 'drive' + | 'drive-files' + | 'instance-requests' + | 'instance-users' + | 'instance-users-total' + | 'instance-notes' + | 'instance-notes-total' + | 'instance-ff' + | 'instance-ff-total' + | 'instance-drive-usage' + | 'instance-drive-usage-total' + | 'instance-drive-files' + | 'instance-drive-files-total' + | 'per-user-notes' + | 'per-user-pv' + | 'per-user-following' + | 'per-user-followers' + | 'per-user-drive' + +const props = withDefaults(defineProps<{ + src: ChartSrc; + args?: { + host?: string; + user?: Misskey.entities.UserLite; + withoutAll?: boolean; + }; + limit?: number; + span: 'hour' | 'day'; + detailed?: boolean; + stacked?: boolean; + bar?: boolean; + aspectRatio?: number | null; +}>(), { + args: undefined, + limit: 90, + detailed: false, + stacked: false, + bar: false, + aspectRatio: null, }); const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>(); diff --git a/packages/frontend/src/components/MkChartLegend.stories.impl.ts b/packages/frontend/src/components/MkChartLegend.stories.impl.ts new file mode 100644 index 0000000000..06146e20e4 --- /dev/null +++ b/packages/frontend/src/components/MkChartLegend.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkChartLegend from './MkChartLegend.vue'; +void MkChartLegend; diff --git a/packages/frontend/src/components/MkChartTooltip.stories.impl.ts b/packages/frontend/src/components/MkChartTooltip.stories.impl.ts new file mode 100644 index 0000000000..289a9e9f27 --- /dev/null +++ b/packages/frontend/src/components/MkChartTooltip.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkChartTooltip from './MkChartTooltip.vue'; +void MkChartTooltip; diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts new file mode 100644 index 0000000000..8378010f8b --- /dev/null +++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { expect, within } from '@storybook/test'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkClickerGame from './MkClickerGame.vue'; + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export const Default = { + render(args) { + return { + components: { + MkClickerGame, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkClickerGame v-bind="props" />', + }; + }, + async play({ canvasElement }) { + await sleep(1000); + const canvas = within(canvasElement); + const count = canvas.getByTestId('count'); + // NOTE: flaky なので N/A も通しておく + await expect(count).toHaveTextContent(/^(0|N\/A)$/); + // FIXME: flaky + // const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); + // await userEvent.click(buttonElement); + // await expect(count).toHaveTextContent('1'); + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/i/registry/get', async ({ request }) => { + action('POST /api/i/registry/get')(await request.json()); + return HttpResponse.json({ + error: { + message: 'No such key.', + code: 'NO_SUCH_KEY', + id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', + }, + }, { + status: 400, + }); + }), + http.post('/api/i/registry/set', async ({ request }) => { + action('POST /api/i/registry/set')(await request.json()); + return HttpResponse.json(undefined, { status: 204 }); + }), + http.post('/api/i/claim-achievement', async ({ request }) => { + action('POST /api/i/claim-achievement')(await request.json()); + return HttpResponse.json(undefined, { status: 204 }); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkClickerGame>; diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 23046bf345..b592609e18 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <div v-if="game.ready" :class="$style.game"> <div :class="$style.cps" class="">{{ number(cps) }}cps</div> - <div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div> + <div :class="$style.count" class="" data-testid="count"><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div> <button v-click-anime class="_button" @click="onClick"> <img src="/client-assets/cookie.png" :class="$style.img"> </button> diff --git a/packages/frontend/src/components/MkClipPreview.stories.impl.ts b/packages/frontend/src/components/MkClipPreview.stories.impl.ts new file mode 100644 index 0000000000..1011254e7a --- /dev/null +++ b/packages/frontend/src/components/MkClipPreview.stories.impl.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { clip } from '../../.storybook/fakes.js'; +import MkClipPreview from './MkClipPreview.vue'; +export const Default = { + render(args) { + return { + components: { + MkClipPreview, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkClipPreview v-bind="props" />', + }; + }, + args: { + clip: clip(), + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + 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>', + }), + ], +} satisfies StoryObj<typeof MkClipPreview>; diff --git a/packages/frontend/src/components/MkCode.core.stories.impl.ts b/packages/frontend/src/components/MkCode.core.stories.impl.ts new file mode 100644 index 0000000000..91990fffd5 --- /dev/null +++ b/packages/frontend/src/components/MkCode.core.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkCode_core from './MkCode.core.vue'; +void MkCode_core; diff --git a/packages/frontend/src/components/MkCode.stories.impl.ts b/packages/frontend/src/components/MkCode.stories.impl.ts new file mode 100644 index 0000000000..b7e53e8e35 --- /dev/null +++ b/packages/frontend/src/components/MkCode.stories.impl.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import MkCode from './MkCode.vue'; +const code = `for (let i, 100) { + <: if (i % 15 == 0) "FizzBuzz" + elif (i % 3 == 0) "Fizz" + elif (i % 5 == 0) "Buzz" + else i +}`; +export const Default = { + render(args) { + return { + components: { + MkCode, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkCode v-bind="props" />', + }; + }, + args: { + code, + lang: 'is', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkCode>; diff --git a/packages/frontend/src/components/MkCodeEditor.stories.impl.ts b/packages/frontend/src/components/MkCodeEditor.stories.impl.ts new file mode 100644 index 0000000000..5c410c4886 --- /dev/null +++ b/packages/frontend/src/components/MkCodeEditor.stories.impl.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { action } from '@storybook/addon-actions'; +import MkCodeEditor from './MkCodeEditor.vue'; +const code = `for (let i, 100) { + <: if (i % 15 == 0) "FizzBuzz" + elif (i % 3 == 0) "Fizz" + elif (i % 5 == 0) "Buzz" + else i +}`; +export const Default = { + render(args) { + return { + components: { + MkCodeEditor, + }, + data() { + return { + code, + }; + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + 'change': action('change'), + 'keydown': action('keydown'), + 'enter': action('enter'), + 'update:modelValue': action('update:modelValue'), + }; + }, + }, + template: '<MkCodeEditor v-model="code" v-bind="props" v-on="events" />', + }; + }, + args: { + lang: 'aiscript', + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 800px; width: 100%; margin: 3rem"><Suspense><story/></Suspense></div></div>', + }), + ], +} satisfies StoryObj<typeof MkCodeEditor>; diff --git a/packages/frontend/src/components/MkCodeInline.stories.impl.ts b/packages/frontend/src/components/MkCodeInline.stories.impl.ts new file mode 100644 index 0000000000..51d4d106ff --- /dev/null +++ b/packages/frontend/src/components/MkCodeInline.stories.impl.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import MkCodeInline from './MkCodeInline.vue'; +export const Default = { + render(args) { + return { + components: { + MkCodeInline, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkCodeInline v-bind="props"/>', + }; + }, + args: { + code: '<: "Hello, world!"', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkCodeInline>; diff --git a/packages/frontend/src/components/MkColorInput.stories.impl.ts b/packages/frontend/src/components/MkColorInput.stories.impl.ts new file mode 100644 index 0000000000..61383e2cae --- /dev/null +++ b/packages/frontend/src/components/MkColorInput.stories.impl.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { action } from '@storybook/addon-actions'; +import MkColorInput from './MkColorInput.vue'; +export const Default = { + render(args) { + return { + components: { + MkColorInput, + }, + data() { + return { + color: '#cccccc', + }; + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + 'update:modelValue': action('update:modelValue'), + }; + }, + }, + template: '<MkColorInput v-model="color" v-bind="props" v-on="events" />', + }; + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 800px; width: 100%; margin: 3rem"><story/></div></div>', + }), + ], +} satisfies StoryObj<typeof MkColorInput>; diff --git a/packages/frontend/src/components/MkContainer.stories.impl.ts b/packages/frontend/src/components/MkContainer.stories.impl.ts new file mode 100644 index 0000000000..72a7659521 --- /dev/null +++ b/packages/frontend/src/components/MkContainer.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkContainer from './MkContainer.vue'; +void MkContainer; diff --git a/packages/frontend/src/components/MkContextMenu.stories.impl.ts b/packages/frontend/src/components/MkContextMenu.stories.impl.ts new file mode 100644 index 0000000000..1ff0f51bd4 --- /dev/null +++ b/packages/frontend/src/components/MkContextMenu.stories.impl.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { userEvent, within } from '@storybook/test'; +import MkContextMenu from './MkContextMenu.vue'; +import * as os from '@/os.js'; +export const Empty = { + render(args) { + return { + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + methods: { + onContextmenu(ev: MouseEvent) { + os.contextMenu(args.items, ev); + }, + }, + template: '<div @contextmenu.stop="onContextmenu">Right Click Here</div>', + }; + }, + args: { + items: [], + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const target = canvas.getByText('Right Click Here'); + await userEvent.pointer({ keys: '[MouseRight>]', target }); + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkContextMenu>; +export const SomeTabs = { + ...Empty, + args: { + items: [ + { + text: 'Home', + icon: 'ti ti-home', + action() {}, + }, + ], + }, +} satisfies StoryObj<typeof MkContextMenu>; diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts new file mode 100644 index 0000000000..ce13093975 --- /dev/null +++ b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { file } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkCropperDialog from './MkCropperDialog.vue'; +export const Default = { + render(args) { + return { + components: { + MkCropperDialog, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + 'ok': action('ok'), + 'cancel': action('cancel'), + 'closed': action('closed'), + }; + }, + }, + template: '<MkCropperDialog v-bind="props" v-on="events" />', + }; + }, + args: { + file: file(), + aspectRatio: NaN, + }, + parameters: { + chromatic: { + // NOTE: ロードが終わるまで待つ + delay: 3000, + }, + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.get('/proxy/image.webp', async ({ request }) => { + const url = new URL(request.url).searchParams.get('url'); + if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') { + const image = await (await fetch('client-assets/fedi.jpg')).blob(); + return new HttpResponse(image, { + headers: { + 'Content-Type': 'image/jpeg', + }, + }); + } else { + return new HttpResponse(null, { status: 404 }); + } + }), + http.post('/api/drive/files/create', async ({ request }) => { + action('POST /api/drive/files/create')(await request.formData()); + return HttpResponse.json(file()); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkCropperDialog>; diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts new file mode 100644 index 0000000000..8a05e06311 --- /dev/null +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { emojiDetailed } from '../../.storybook/fakes.js'; +import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue'; +export const Default = { + render(args) { + return { + components: { + MkCustomEmojiDetailedDialog, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkCustomEmojiDetailedDialog v-bind="props" />', + }; + }, + args: { + emoji: emojiDetailed(), + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkCustomEmojiDetailedDialog>; diff --git a/packages/frontend/src/components/MkCwButton.stories.impl.ts b/packages/frontend/src/components/MkCwButton.stories.impl.ts new file mode 100644 index 0000000000..05c6001552 --- /dev/null +++ b/packages/frontend/src/components/MkCwButton.stories.impl.ts @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { action } from '@storybook/addon-actions'; +import { expect, userEvent, within } from '@storybook/test'; +import { file } from '../../.storybook/fakes.js'; +import MkCwButton from './MkCwButton.vue'; +import { i18n } from '@/i18n.js'; +import { semaphore } from '@/scripts/test-utils.js'; + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +const s = semaphore(); + +export const Default = { + render(args) { + return { + components: { + MkCwButton, + }, + data() { + return { + showContent: false, + }; + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + 'update:modelValue': action('update:modelValue'), + }; + }, + }, + template: '<MkCwButton v-model="showContent" v-bind="props" v-on="events" />', + }; + }, + args: { + text: 'Some CW content', + }, + async play({ canvasElement }) { + await s.acquire(); + await sleep(1000); + const canvas = within(canvasElement); + const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); + await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show); + await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); + await userEvent.click(buttonElement); + await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide); + await userEvent.click(buttonElement); + s.release(); + }, + parameters: { + chromatic: { + // NOTE: テストが終わるまで待つ + delay: 5000, + }, + layout: 'centered', + }, +} satisfies StoryObj<typeof MkCwButton>; +export const IncludesTextAndDriveFile = { + ...Default, + args: { + text: 'Some CW content', + files: [file()], + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); + await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); + await expect(buttonElement).toHaveTextContent(' / '); + await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.files({ count: 1 })); + }, +} satisfies StoryObj<typeof MkCwButton>; |