diff options
| author | Acid Chicken (硫酸鶏) <root@acid-chicken.com> | 2023-04-04 09:38:34 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-04-04 09:38:34 +0900 |
| commit | 38d0b6216715feb3c3d8e78d9256b5f63ccf5500 (patch) | |
| tree | dea120f5f1293b8eb704729a8e2da3cac9333e0f /packages/frontend/src/components | |
| parent | add notes (diff) | |
| download | sharkey-38d0b6216715feb3c3d8e78d9256b5f63ccf5500.tar.gz sharkey-38d0b6216715feb3c3d8e78d9256b5f63ccf5500.tar.bz2 sharkey-38d0b6216715feb3c3d8e78d9256b5f63ccf5500.zip | |
build(#10336): Storybook & Chromatic & msw (#10365)
* build(#10336): init
* fix(#10336): invalid name conversion
* build(#10336): load locales and vite config
* refactor(#10336): remove unused imports
* build(#10336): separate definitions and generated codes
* refactor(#10336): remove hatches
* refactor(#10336): module semantics
* refactor(#10336): remove unused common preferences
* fix: typo
* build(#10336): mock assets
* build(#10336): impl `SatisfiesExpression`
* build(#10336): control themes
* refactor(#10336): semantics
* build(#10336): make .storybook as an individual TypeScript project
* style(#10336): use single quote
* build(#10336): avoid intrinsic component names
* chore: suppress linter
* style: typing
* build(#10336): update dependencies
* docs: note about Storybook
* build(#10336): sync
* build(#10336): full reload server on change
* chore: use defaultStore instead
* build(#10336): show popups on Story
* refactor(#10336): remove redundant div
* docs: fix
* build(#10336): interactions
* build(#10336): add an interaction test for `<MkA/>`
* build(#10336): bump storybook
* docs(#10336): mention to pre-build misskey-js
* build(#10336): write stories for `MkAcct`
* build(#10336): write stories for `MkAd`
* build(#10336): fix missing type definition
* build(#10336): use `toHaveTextContent`
* build(#10336): write some stories
* build(#10336): hide internal args
* build(#10336): generate `components/global` stories only
* build(#10336): write stories for `MkMisskeyFlavoredMarkdown`
* fix: conflict errors
* build(#10336): subcomponents on sidebar
* refactor: restore `SatisfiesExpression`
* docs(#10336): note development status
* build(#10336): use chokidar-cli
* docs(#10336): note chokidar-cli mode
* chore(#10336): untrack generated stories files
* fix: pointer handling
* build(#10336): finalize
* chore: add static option to `MkLoading`
* refactor(#10336): bind to local args
* fix: missing case
* revert: restore `SatisfiesExpression`
This reverts commit f246699f38a28befbfccc11e9eade22cbaace4f3.
* build(#10336): make storybook buildable
* build(#10336): staticify assets
* build(#10336): staticified directory structure
* build(#10336): normalize path for Windows
* ci(#10336): create actions
* build(#10336): ignore tsc errors
* build(#10336): ignore tsc errors
* build(#10336): missing dependencies
* build(#10336): missing dependencies
* build(#10336): use fast-glob
* fix: invalid lockfile
* ci(#10336): increase heap size
* build(#10336): use unpkg for storybook tabler icons
* build(#10336): use unpkg for storybook twemojis
* build(#10336): disable `ProfilePageCat`
* build(#10336): blur `MkA` before interaction ends
* ci(#10336): stabilize
* ci(#10336): fetch-depth
* build(#10336): isChromatic
* ci(#10336): notify on changes
* ci(#10336): fix typo
* ci(#10336): missing working directory
* ci(#10336): skip build
* ci(#10336): fix path
* build(#10336): fails on Windows
* build(#10336): available on Windows
* ci(#10336): disable animation on chromatic
* ci(#10336): add static option to `PageHeader.tabs`
* chore: void
* ci(#10336): change parameters
* docs(#10336): update CONTRIBUTING
* docs(#10336): note about meta overriding and etc.
* ci(#10336): use Chromatic for checks
* ci(#10336): use `pull_request` instead of `pull_request_target` for now
* ci(#10336): use `exitOnceUploaded`
* ci(#10336): reuse built storybook
* ci(#10336): back to `pull_request_target`
* chore: unused dependencies
* style(#10336): reduce prettier indents
* style: note about `TSSatisfiesExpression`
Diffstat (limited to 'packages/frontend/src/components')
31 files changed, 1186 insertions, 26 deletions
diff --git a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts new file mode 100644 index 0000000000..05190aa268 --- /dev/null +++ b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import MkAnalogClock from './MkAnalogClock.vue'; +export const Default = { + render(args) { + return { + components: { + MkAnalogClock, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkAnalogClock v-bind="props" />', + }; + }, + parameters: { + layout: 'fullscreen', + }, +} satisfies StoryObj<typeof MkAnalogClock>; diff --git a/packages/frontend/src/components/MkButton.stories.impl.ts b/packages/frontend/src/components/MkButton.stories.impl.ts new file mode 100644 index 0000000000..e1c1c54d10 --- /dev/null +++ b/packages/frontend/src/components/MkButton.stories.impl.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +/* eslint-disable import/no-duplicates */ +import { StoryObj } from '@storybook/vue3'; +import MkButton from './MkButton.vue'; +export const Default = { + render(args) { + return { + components: { + MkButton, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkButton v-bind="props">Text</MkButton>', + }; + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkButton>; diff --git a/packages/frontend/src/components/MkCaptcha.stories.impl.ts b/packages/frontend/src/components/MkCaptcha.stories.impl.ts new file mode 100644 index 0000000000..6ac437a277 --- /dev/null +++ b/packages/frontend/src/components/MkCaptcha.stories.impl.ts @@ -0,0 +1,2 @@ +import MkCaptcha from './MkCaptcha.vue'; +void MkCaptcha; diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index 5bdf477241..b81c806b0c 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -17,8 +17,8 @@ import { onMounted, onBeforeUnmount } from 'vue'; import MkMenu from './MkMenu.vue'; import { MenuItem } from './types/menu.vue'; import contains from '@/scripts/contains'; -import * as os from '@/os'; import { defaultStore } from '@/store'; +import * as os from '@/os'; const props = defineProps<{ items: MenuItem[]; diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 9e3022896c..e513a65a32 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -1,5 +1,5 @@ <template> -<div> +<div role="menu"> <div ref="itemsEl" v-hotkey="keymap" class="_popup _shadow" @@ -8,37 +8,37 @@ @contextmenu.self="e => e.preventDefault()" > <template v-for="(item, i) in items2"> - <div v-if="item === null" :class="$style.divider"></div> - <span v-else-if="item.type === 'label'" :class="[$style.label, $style.item]"> + <div v-if="item === null" role="separator" :class="$style.divider"></div> + <span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]"> <span>{{ item.text }}</span> </span> - <span v-else-if="item.type === 'pending'" :tabindex="i" :class="[$style.pending, $style.item]"> + <span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]"> <span><MkEllipsis/></span> </span> - <MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <span>{{ item.text }}</span> <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> </MkA> - <a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <span>{{ item.text }}</span> <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> </a> - <button v-else-if="item.type === 'user'" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/> <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> </button> - <span v-else-if="item.type === 'switch'" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <span v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch> </span> - <button v-else-if="item.type === 'parent'" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)"> + <button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <span>{{ item.text }}</span> <span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span> </button> - <button v-else :tabindex="i" class="_button" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <span>{{ item.text }}</span> diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 635ac3e8bd..9c5622b1c5 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -150,7 +150,7 @@ function adjustTweetHeight(message: any) { } const openPlayer = (): void => { - os.popup(defineAsyncComponent(() => import('@/components/MkYoutubePlayer.vue')), { + os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), { url: requestUrl.href, }); }; diff --git a/packages/frontend/src/components/MkYoutubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue index 4d765fe2f7..4d765fe2f7 100644 --- a/packages/frontend/src/components/MkYoutubePlayer.vue +++ b/packages/frontend/src/components/MkYouTubePlayer.vue diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts new file mode 100644 index 0000000000..72d069e853 --- /dev/null +++ b/packages/frontend/src/components/global/MkA.stories.impl.ts @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { expect } from '@storybook/jest'; +import { userEvent, within } from '@storybook/testing-library'; +import { StoryObj } from '@storybook/vue3'; +import MkA from './MkA.vue'; +import { tick } from '@/scripts/test-utils'; +export const Default = { + render(args) { + return { + components: { + MkA, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkA v-bind="props">Text</MkA>', + }; + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const a = canvas.getByRole<HTMLAnchorElement>('link'); + await expect(a.href).toMatch(/^https?:\/\/.*#test$/); + await userEvent.click(a, { button: 2 }); + await tick(); + const menu = canvas.getByRole('menu'); + await expect(menu).toBeInTheDocument(); + await userEvent.click(a, { button: 0 }); + a.blur(); + await tick(); + await expect(menu).not.toBeInTheDocument(); + }, + args: { + to: '#test', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkA>; diff --git a/packages/frontend/src/components/global/MkAcct.stories.impl.ts b/packages/frontend/src/components/global/MkAcct.stories.impl.ts new file mode 100644 index 0000000000..7dfa1a14f2 --- /dev/null +++ b/packages/frontend/src/components/global/MkAcct.stories.impl.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import { userDetailed } from '../../../.storybook/fakes'; +import MkAcct from './MkAcct.vue'; +export const Default = { + render(args) { + return { + components: { + MkAcct, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkAcct v-bind="props" />', + }; + }, + args: { + user: { + ...userDetailed, + host: null, + }, + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkAcct>; +export const Detail = { + ...Default, + args: { + ...Default.args, + user: userDetailed, + detail: true, + }, +} satisfies StoryObj<typeof MkAcct>; diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue index e06ab64e86..2b9f892fc6 100644 --- a/packages/frontend/src/components/global/MkAcct.vue +++ b/packages/frontend/src/components/global/MkAcct.vue @@ -18,4 +18,3 @@ defineProps<{ const host = toUnicode(hostRaw); </script> - diff --git a/packages/frontend/src/components/global/MkAd.stories.impl.ts b/packages/frontend/src/components/global/MkAd.stories.impl.ts new file mode 100644 index 0000000000..7d8a42a03c --- /dev/null +++ b/packages/frontend/src/components/global/MkAd.stories.impl.ts @@ -0,0 +1,120 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { expect } from '@storybook/jest'; +import { userEvent, within } from '@storybook/testing-library'; +import { StoryObj } from '@storybook/vue3'; +import { i18n } from '@/i18n'; +import MkAd from './MkAd.vue'; +const common = { + render(args) { + return { + components: { + MkAd, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkAd v-bind="props" />', + }; + }, + async play({ canvasElement, args }) { + const canvas = within(canvasElement); + const a = canvas.getByRole<HTMLAnchorElement>('link'); + await expect(a.href).toMatch(/^https?:\/\/.*#test$/); + const img = within(a).getByRole('img'); + await expect(img).toBeInTheDocument(); + let buttons = canvas.getAllByRole<HTMLButtonElement>('button'); + await expect(buttons).toHaveLength(1); + const i = buttons[0]; + await expect(i).toBeInTheDocument(); + await userEvent.click(i); + await expect(a).not.toBeInTheDocument(); + await expect(i).not.toBeInTheDocument(); + buttons = canvas.getAllByRole<HTMLButtonElement>('button'); + await expect(buttons).toHaveLength(args.__hasReduce ? 2 : 1); + const reduce = args.__hasReduce ? buttons[0] : null; + const back = buttons[args.__hasReduce ? 1 : 0]; + if (reduce) { + await expect(reduce).toBeInTheDocument(); + await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd); + } + await expect(back).toBeInTheDocument(); + await expect(back).toHaveTextContent(i18n.ts._ad.back); + await userEvent.click(back); + if (reduce) { + await expect(reduce).not.toBeInTheDocument(); + } + await expect(back).not.toBeInTheDocument(); + const aAgain = canvas.getByRole<HTMLAnchorElement>('link'); + await expect(aAgain).toBeInTheDocument(); + const imgAgain = within(aAgain).getByRole('img'); + await expect(imgAgain).toBeInTheDocument(); + }, + args: { + prefer: [], + specify: { + id: 'someadid', + radio: 1, + url: '#test', + }, + __hasReduce: true, + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkAd>; +export const Square = { + ...common, + args: { + ...common.args, + specify: { + ...common.args.specify, + place: 'square', + imageUrl: + 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', + }, + }, +} satisfies StoryObj<typeof MkAd>; +export const Horizontal = { + ...common, + args: { + ...common.args, + specify: { + ...common.args.specify, + place: 'horizontal', + imageUrl: + 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', + }, + }, +} satisfies StoryObj<typeof MkAd>; +export const HorizontalBig = { + ...common, + args: { + ...common.args, + specify: { + ...common.args.specify, + place: 'horizontal-big', + imageUrl: + 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', + }, + }, +} satisfies StoryObj<typeof MkAd>; +export const ZeroRatio = { + ...Square, + args: { + ...Square.args, + specify: { + ...Square.args.specify, + ratio: 0, + }, + __hasReduce: false, + }, +} satisfies StoryObj<typeof MkAd>; diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index b8f749bd1c..5799f99d5f 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -20,13 +20,13 @@ <script lang="ts" setup> import { ref } from 'vue'; +import { i18n } from '@/i18n'; import { instance } from '@/instance'; import { host } from '@/config'; import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store'; import * as os from '@/os'; import { $i } from '@/account'; -import { i18n } from '@/i18n'; type Ad = (typeof instance)['ads'][number]; diff --git a/packages/frontend/src/components/global/MkAvatar.stories.impl.ts b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts new file mode 100644 index 0000000000..6c46f75b5f --- /dev/null +++ b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts @@ -0,0 +1,66 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import { userDetailed } from '../../../.storybook/fakes'; +import MkAvatar from './MkAvatar.vue'; +const common = { + render(args) { + return { + components: { + MkAvatar, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkAvatar v-bind="props" />', + }; + }, + args: { + user: userDetailed, + }, + decorators: [ + (Story, context) => ({ + // eslint-disable-next-line quotes + template: `<div :style="{ display: 'grid', width: '${context.args.size}px', height: '${context.args.size}px' }"><story/></div>`, + }), + ], + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkAvatar>; +export const ProfilePage = { + ...common, + args: { + ...common.args, + size: 120, + indicator: true, + }, +} satisfies StoryObj<typeof MkAvatar>; +export const ProfilePageCat = { + ...ProfilePage, + args: { + ...ProfilePage.args, + user: { + ...userDetailed, + isCat: true, + }, + }, + parameters: { + ...ProfilePage.parameters, + chromatic: { + /* Your story couldn’t be captured because it exceeds our 25,000,000px limit. Its dimensions are 5,504,893x5,504,892px. Possible ways to resolve: + * * Separate pages into components + * * Minimize the number of very large elements in a story + */ + disableSnapshot: true, + }, + }, +} satisfies StoryObj<typeof MkAvatar>; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 9a21941c8d..0cc30a887f 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -148,6 +148,7 @@ watch(() => props.user.avatarBlurhash, () => { width: 100%; height: 100%; padding: 50%; + pointer-events: none; &.mask { -webkit-mask: diff --git a/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts b/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts new file mode 100644 index 0000000000..36ab85b579 --- /dev/null +++ b/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import MkCustomEmoji from './MkCustomEmoji.vue'; +export const Default = { + render(args) { + return { + components: { + MkCustomEmoji, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkCustomEmoji v-bind="props" />', + }; + }, + args: { + name: 'mi', + url: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkCustomEmoji>; +export const Normal = { + ...Default, + args: { + ...Default.args, + normal: true, + }, +} satisfies StoryObj<typeof MkCustomEmoji>; +export const Missing = { + ...Default, + args: { + name: Default.args.name, + }, +} satisfies StoryObj<typeof MkCustomEmoji>; diff --git a/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts b/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts new file mode 100644 index 0000000000..65405a9bc8 --- /dev/null +++ b/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import isChromatic from 'chromatic/isChromatic'; +import MkEllipsis from './MkEllipsis.vue'; +export const Default = { + render(args) { + return { + components: { + MkEllipsis, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkEllipsis v-bind="props" />', + }; + }, + args: { + static: isChromatic(), + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkEllipsis>; diff --git a/packages/frontend/src/components/global/MkEllipsis.vue b/packages/frontend/src/components/global/MkEllipsis.vue index b3cf69c075..c8f6cd3394 100644 --- a/packages/frontend/src/components/global/MkEllipsis.vue +++ b/packages/frontend/src/components/global/MkEllipsis.vue @@ -1,9 +1,19 @@ <template> -<span :class="$style.root"> +<span :class="[$style.root, { [$style.static]: static }]"> <span :class="$style.dot">.</span><span :class="$style.dot">.</span><span :class="$style.dot">.</span> </span> </template> +<script lang="ts" setup> +import { } from 'vue'; + +const props = withDefaults(defineProps<{ + static?: boolean; +}>(), { + static: false, +}); +</script> + <style lang="scss" module> @keyframes ellipsis { 0%, 80%, 100% { @@ -15,7 +25,9 @@ } .root { - + &.static > .dot { + animation-play-state: paused; + } } .dot { diff --git a/packages/frontend/src/components/global/MkEmoji.stories.impl.ts b/packages/frontend/src/components/global/MkEmoji.stories.impl.ts new file mode 100644 index 0000000000..f9900375f7 --- /dev/null +++ b/packages/frontend/src/components/global/MkEmoji.stories.impl.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import MkEmoji from './MkEmoji.vue'; +export const Default = { + render(args) { + return { + components: { + MkEmoji, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkEmoji v-bind="props" />', + }; + }, + args: { + emoji: '❤', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkEmoji>; diff --git a/packages/frontend/src/components/global/MkError.stories.meta.ts b/packages/frontend/src/components/global/MkError.stories.meta.ts new file mode 100644 index 0000000000..51d763ada7 --- /dev/null +++ b/packages/frontend/src/components/global/MkError.stories.meta.ts @@ -0,0 +1,5 @@ +export const argTypes = { + retry: { + action: 'retry', + }, +}; diff --git a/packages/frontend/src/components/global/MkLoading.stories.impl.ts b/packages/frontend/src/components/global/MkLoading.stories.impl.ts new file mode 100644 index 0000000000..9dcc0cdea1 --- /dev/null +++ b/packages/frontend/src/components/global/MkLoading.stories.impl.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import isChromatic from 'chromatic/isChromatic'; +import MkLoading from './MkLoading.vue'; +export const Default = { + render(args) { + return { + components: { + MkLoading, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkLoading v-bind="props" />', + }; + }, + args: { + static: isChromatic(), + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkLoading>; +export const Inline = { + ...Default, + args: { + ...Default.args, + inline: true, + }, +} satisfies StoryObj<typeof MkLoading>; +export const Colored = { + ...Default, + args: { + ...Default.args, + colored: true, + }, +} satisfies StoryObj<typeof MkLoading>; +export const Mini = { + ...Default, + args: { + ...Default.args, + mini: true, + }, +} satisfies StoryObj<typeof MkLoading>; +export const Em = { + ...Default, + args: { + ...Default.args, + em: true, + }, +} satisfies StoryObj<typeof MkLoading>; diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue index 64e12e3b44..4311f9fe8a 100644 --- a/packages/frontend/src/components/global/MkLoading.vue +++ b/packages/frontend/src/components/global/MkLoading.vue @@ -6,7 +6,7 @@ <circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/> </g> </svg> - <svg :class="[$style.spinner, $style.fg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg"> + <svg :class="[$style.spinner, $style.fg, { [$style.static]: static }]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg"> <g transform="matrix(1.125,0,0,1.125,12,12)"> <path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/> </g> @@ -19,11 +19,13 @@ import { } from 'vue'; const props = withDefaults(defineProps<{ + static?: boolean; inline?: boolean; colored?: boolean; mini?: boolean; em?: boolean; }>(), { + static: false, inline: false, colored: true, mini: false, @@ -97,5 +99,9 @@ const props = withDefaults(defineProps<{ .fg { animation: spinner 0.5s linear infinite; + + &.static { + animation-play-state: paused; + } } </style> diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts new file mode 100644 index 0000000000..f6811b6747 --- /dev/null +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.vue'; +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +export const Default = { + render(args) { + return { + components: { + MkMisskeyFlavoredMarkdown, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkMisskeyFlavoredMarkdown v-bind="props" />', + }; + }, + async play({ canvasElement, args }) { + const canvas = within(canvasElement); + if (args.plain) { + const aiHelloMiskist = canvas.getByText('@ai *Hello*, #Miskist!'); + await expect(aiHelloMiskist).toBeInTheDocument(); + } else { + const ai = canvas.getByText('@ai'); + await expect(ai).toBeInTheDocument(); + await expect(ai.closest('a')).toHaveAttribute('href', '/@ai'); + const hello = canvas.getByText('Hello'); + await expect(hello).toBeInTheDocument(); + await expect(hello.style.fontStyle).toBe('oblique'); + const miskist = canvas.getByText('#Miskist'); + await expect(miskist).toBeInTheDocument(); + await expect(miskist).toHaveAttribute('href', args.isNote ?? true ? '/tags/Miskist' : '/user-tags/Miskist'); + } + const heart = canvas.getByAltText('❤'); + await expect(heart).toBeInTheDocument(); + await expect(heart).toHaveAttribute('src', '/twemoji/2764.svg'); + }, + args: { + text: '@ai *Hello*, #Miskist! ❤', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +export const Plain = { + ...Default, + args: { + ...Default.args, + plain: true, + }, +} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +export const Nowrap = { + ...Default, + args: { + ...Default.args, + nowrap: true, + }, +} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +export const IsNotNote = { + ...Default, + args: { + ...Default.args, + isNote: false, + }, +} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts new file mode 100644 index 0000000000..5519d60fc4 --- /dev/null +++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts @@ -0,0 +1,98 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import MkPageHeader from './MkPageHeader.vue'; +export const Empty = { + render(args) { + return { + components: { + MkPageHeader, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkPageHeader v-bind="props" />', + }; + }, + args: { + static: true, + tabs: [], + }, + parameters: { + layout: 'centered', + chromatic: { + /* This component has animations that are implemented with JavaScript. So it's unstable to take a snapshot. */ + disableSnapshot: true, + }, + }, +} satisfies StoryObj<typeof MkPageHeader>; +export const OneTab = { + ...Empty, + args: { + ...Empty.args, + tab: 'sometabkey', + tabs: [ + { + key: 'sometabkey', + title: 'Some Tab Title', + }, + ], + }, +} satisfies StoryObj<typeof MkPageHeader>; +export const Icon = { + ...OneTab, + args: { + ...OneTab.args, + tabs: [ + { + ...OneTab.args.tabs[0], + icon: 'ti ti-home', + }, + ], + }, +} satisfies StoryObj<typeof MkPageHeader>; +export const IconOnly = { + ...Icon, + args: { + ...Icon.args, + tabs: [ + { + ...Icon.args.tabs[0], + title: undefined, + iconOnly: true, + }, + ], + }, +} satisfies StoryObj<typeof MkPageHeader>; +export const SomeTabs = { + ...Empty, + args: { + ...Empty.args, + tab: 'princess', + tabs: [ + { + key: 'princess', + title: 'Princess', + icon: 'ti ti-crown', + }, + { + key: 'fairy', + title: 'Fairy', + icon: 'ti ti-snowflake', + }, + { + key: 'angel', + title: 'Angel', + icon: 'ti ti-feather', + }, + ], + }, +} satisfies StoryObj<typeof MkPageHeader>; diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts new file mode 100644 index 0000000000..6d4460d593 --- /dev/null +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts @@ -0,0 +1,3 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import MkPageHeader_tabs from './MkPageHeader.tabs.vue'; +void MkPageHeader_tabs; diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 42760da08f..9e1da64e61 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -33,14 +33,18 @@ <script lang="ts"> export type Tab = { key: string; - title: string; - icon?: string; - iconOnly?: boolean; onClick?: (ev: MouseEvent) => void; -} & { - iconOnly: true; - iccn: string; -}; +} & ( + | { + iconOnly?: false; + title: string; + icon?: string; + } + | { + iconOnly: true; + icon: string; + } +); </script> <script lang="ts" setup> diff --git a/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts b/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts new file mode 100644 index 0000000000..97b8cc0c5b --- /dev/null +++ b/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts @@ -0,0 +1,3 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import MkStickyContainer from './MkStickyContainer.vue'; +void MkStickyContainer; diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts new file mode 100644 index 0000000000..b72601b1ff --- /dev/null +++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts @@ -0,0 +1,312 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { expect } from '@storybook/jest'; +import { StoryObj } from '@storybook/vue3'; +import MkTime from './MkTime.vue'; +import { i18n } from '@/i18n'; +import { dateTimeFormat } from '@/scripts/intl-const'; +const now = new Date('2023-04-01T00:00:00.000Z'); +const future = new Date(8640000000000000); +const oneHourAgo = new Date(now.getTime() - 3600000); +const oneDayAgo = new Date(now.getTime() - 86400000); +const oneWeekAgo = new Date(now.getTime() - 604800000); +const oneMonthAgo = new Date(now.getTime() - 2592000000); +const oneYearAgo = new Date(now.getTime() - 31536000000); +export const Empty = { + render(args) { + return { + components: { + MkTime, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkTime v-bind="props" />', + }; + }, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(i18n.ts._ago.invalid); + }, + args: { + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkTime>; +export const RelativeFuture = { + ...Empty, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(i18n.ts._ago.future); + }, + args: { + ...Empty.args, + time: future, + }, +} satisfies StoryObj<typeof MkTime>; +export const AbsoluteFuture = { + ...Empty, + async play({ canvasElement, args }) { + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + }, + args: { + ...Empty.args, + time: future, + mode: 'absolute', + }, +} satisfies StoryObj<typeof MkTime>; +export const DetailFuture = { + ...Empty, + async play(context) { + await AbsoluteFuture.play(context); + await expect(context.canvasElement).toHaveTextContent(' ('); + await RelativeFuture.play(context); + await expect(context.canvasElement).toHaveTextContent(')'); + }, + args: { + ...Empty.args, + time: future, + mode: 'detail', + }, +} satisfies StoryObj<typeof MkTime>; +export const RelativeNow = { + ...Empty, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(i18n.ts._ago.justNow); + }, + args: { + ...Empty.args, + time: now, + origin: now, + mode: 'relative', + }, +} satisfies StoryObj<typeof MkTime>; +export const AbsoluteNow = { + ...Empty, + async play({ canvasElement, args }) { + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + }, + args: { + ...Empty.args, + time: now, + origin: now, + mode: 'absolute', + }, +} satisfies StoryObj<typeof MkTime>; +export const DetailNow = { + ...Empty, + async play(context) { + await AbsoluteNow.play(context); + await expect(context.canvasElement).toHaveTextContent(' ('); + await RelativeNow.play(context); + await expect(context.canvasElement).toHaveTextContent(')'); + }, + args: { + ...Empty.args, + time: now, + origin: now, + mode: 'detail', + }, +} satisfies StoryObj<typeof MkTime>; +export const RelativeOneHourAgo = { + ...Empty, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(i18n.t('_ago.hoursAgo', { n: 1 })); + }, + args: { + ...Empty.args, + time: oneHourAgo, + origin: now, + mode: 'relative', + }, +} satisfies StoryObj<typeof MkTime>; +export const AbsoluteOneHourAgo = { + ...Empty, + async play({ canvasElement, args }) { + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + }, + args: { + ...Empty.args, + time: oneHourAgo, + origin: now, + mode: 'absolute', + }, +} satisfies StoryObj<typeof MkTime>; +export const DetailOneHourAgo = { + ...Empty, + async play(context) { + await AbsoluteOneHourAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(' ('); + await RelativeOneHourAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(')'); + }, + args: { + ...Empty.args, + time: oneHourAgo, + origin: now, + mode: 'detail', + }, +} satisfies StoryObj<typeof MkTime>; +export const RelativeOneDayAgo = { + ...Empty, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(i18n.t('_ago.daysAgo', { n: 1 })); + }, + args: { + ...Empty.args, + time: oneDayAgo, + origin: now, + mode: 'relative', + }, +} satisfies StoryObj<typeof MkTime>; +export const AbsoluteOneDayAgo = { + ...Empty, + async play({ canvasElement, args }) { + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + }, + args: { + ...Empty.args, + time: oneDayAgo, + origin: now, + mode: 'absolute', + }, +} satisfies StoryObj<typeof MkTime>; +export const DetailOneDayAgo = { + ...Empty, + async play(context) { + await AbsoluteOneDayAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(' ('); + await RelativeOneDayAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(')'); + }, + args: { + ...Empty.args, + time: oneDayAgo, + origin: now, + mode: 'detail', + }, +} satisfies StoryObj<typeof MkTime>; +export const RelativeOneWeekAgo = { + ...Empty, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(i18n.t('_ago.weeksAgo', { n: 1 })); + }, + args: { + ...Empty.args, + time: oneWeekAgo, + origin: now, + mode: 'relative', + }, +} satisfies StoryObj<typeof MkTime>; +export const AbsoluteOneWeekAgo = { + ...Empty, + async play({ canvasElement, args }) { + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + }, + args: { + ...Empty.args, + time: oneWeekAgo, + origin: now, + mode: 'absolute', + }, +} satisfies StoryObj<typeof MkTime>; +export const DetailOneWeekAgo = { + ...Empty, + async play(context) { + await AbsoluteOneWeekAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(' ('); + await RelativeOneWeekAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(')'); + }, + args: { + ...Empty.args, + time: oneWeekAgo, + origin: now, + mode: 'detail', + }, +} satisfies StoryObj<typeof MkTime>; +export const RelativeOneMonthAgo = { + ...Empty, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(i18n.t('_ago.monthsAgo', { n: 1 })); + }, + args: { + ...Empty.args, + time: oneMonthAgo, + origin: now, + mode: 'relative', + }, +} satisfies StoryObj<typeof MkTime>; +export const AbsoluteOneMonthAgo = { + ...Empty, + async play({ canvasElement, args }) { + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + }, + args: { + ...Empty.args, + time: oneMonthAgo, + origin: now, + mode: 'absolute', + }, +} satisfies StoryObj<typeof MkTime>; +export const DetailOneMonthAgo = { + ...Empty, + async play(context) { + await AbsoluteOneMonthAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(' ('); + await RelativeOneMonthAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(')'); + }, + args: { + ...Empty.args, + time: oneMonthAgo, + origin: now, + mode: 'detail', + }, +} satisfies StoryObj<typeof MkTime>; +export const RelativeOneYearAgo = { + ...Empty, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(i18n.t('_ago.yearsAgo', { n: 1 })); + }, + args: { + ...Empty.args, + time: oneYearAgo, + origin: now, + mode: 'relative', + }, +} satisfies StoryObj<typeof MkTime>; +export const AbsoluteOneYearAgo = { + ...Empty, + async play({ canvasElement, args }) { + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + }, + args: { + ...Empty.args, + time: oneYearAgo, + origin: now, + mode: 'absolute', + }, +} satisfies StoryObj<typeof MkTime>; +export const DetailOneYearAgo = { + ...Empty, + async play(context) { + await AbsoluteOneYearAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(' ('); + await RelativeOneYearAgo.play(context); + await expect(context.canvasElement).toHaveTextContent(')'); + }, + args: { + ...Empty.args, + time: oneYearAgo, + origin: now, + mode: 'detail', + }, +} satisfies StoryObj<typeof MkTime>; diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 3fa8bb9adc..99169512db 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -14,8 +14,10 @@ import { dateTimeFormat } from '@/scripts/intl-const'; const props = withDefaults(defineProps<{ time: Date | string | number | null; + origin?: Date | null; mode?: 'relative' | 'absolute' | 'detail'; }>(), { + origin: null, mode: 'relative', }); @@ -25,7 +27,7 @@ const _time = props.time == null ? NaN : const invalid = Number.isNaN(_time); const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid; -let now = $ref((new Date()).getTime()); +let now = $ref((props.origin ?? new Date()).getTime()); const relative = $computed<string>(() => { if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない if (invalid) return i18n.ts._ago.invalid; @@ -46,7 +48,7 @@ const relative = $computed<string>(() => { let tickId: number; function tick() { - now = (new Date()).getTime(); + now = props.origin ?? (new Date()).getTime(); const ago = (now - _time) / 1000/*ms*/; const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000; diff --git a/packages/frontend/src/components/global/MkUrl.stories.impl.ts b/packages/frontend/src/components/global/MkUrl.stories.impl.ts new file mode 100644 index 0000000000..2344c4851a --- /dev/null +++ b/packages/frontend/src/components/global/MkUrl.stories.impl.ts @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { expect } from '@storybook/jest'; +import { userEvent, within } from '@storybook/testing-library'; +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { commonHandlers } from '../../../.storybook/mocks'; +import MkUrl from './MkUrl.vue'; +export const Default = { + render(args) { + return { + components: { + MkUrl, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkUrl v-bind="props">Text</MkUrl>', + }; + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const a = canvas.getByRole<HTMLAnchorElement>('link'); + await expect(a).toHaveAttribute('href', 'https://misskey-hub.net/'); + await userEvent.hover(a); + /* + await tick(); // FIXME: wait for network request + const anchors = canvas.getAllByRole<HTMLAnchorElement>('link'); + const popup = anchors.find(anchor => anchor !== a)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + await expect(popup).toBeInTheDocument(); + await expect(popup).toHaveAttribute('href', 'https://misskey-hub.net/'); + await expect(popup).toHaveTextContent('Misskey Hub'); + await expect(popup).toHaveTextContent('Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。'); + await expect(popup).toHaveTextContent('misskey-hub.net'); + const icon = within(popup).getByRole('img'); + await expect(icon).toBeInTheDocument(); + await expect(icon).toHaveAttribute('src', 'https://misskey-hub.net/favicon.ico'); + */ + await userEvent.unhover(a); + }, + args: { + url: 'https://misskey-hub.net/', + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + rest.get('/url', (req, res, ctx) => { + return res(ctx.json({ + title: 'Misskey Hub', + icon: 'https://misskey-hub.net/favicon.ico', + description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。', + thumbnail: null, + player: { + url: null, + width: null, + height: null, + allow: [], + }, + sitename: 'misskey-hub.net', + sensitive: false, + url: 'https://misskey-hub.net/', + })); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkUrl>; diff --git a/packages/frontend/src/components/global/MkUserName.stories.impl.ts b/packages/frontend/src/components/global/MkUserName.stories.impl.ts new file mode 100644 index 0000000000..41b1567a6f --- /dev/null +++ b/packages/frontend/src/components/global/MkUserName.stories.impl.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { expect } from '@storybook/jest'; +import { userEvent, within } from '@storybook/testing-library'; +import { StoryObj } from '@storybook/vue3'; +import { userDetailed } from '../../../.storybook/fakes'; +import MkUserName from './MkUserName.vue'; +export const Default = { + render(args) { + return { + components: { + MkUserName, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkUserName v-bind="props"/>', + }; + }, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(userDetailed.name); + }, + args: { + user: userDetailed, + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkUserName>; +export const Anonymous = { + ...Default, + async play({ canvasElement }) { + await expect(canvasElement).toHaveTextContent(userDetailed.username); + }, + args: { + ...Default.args, + user: { + ...userDetailed, + name: null, + }, + }, +} satisfies StoryObj<typeof MkUserName>; +export const Wrap = { + ...Default, + args: { + ...Default.args, + nowrap: false, + }, +} satisfies StoryObj<typeof MkUserName>; diff --git a/packages/frontend/src/components/global/RouterView.stories.impl.ts b/packages/frontend/src/components/global/RouterView.stories.impl.ts new file mode 100644 index 0000000000..7910b8b3cb --- /dev/null +++ b/packages/frontend/src/components/global/RouterView.stories.impl.ts @@ -0,0 +1,3 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import RouterView from './RouterView.vue'; +void RouterView; |