From 31233467ac72f3127932bfd1bba0c32620203753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Tue, 4 Apr 2023 22:14:17 +0900 Subject: [PATCH 01/12] chore(#10336): register snippets --- .../frontend/.vscode/storybook.code-snippets | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/frontend/.vscode/storybook.code-snippets diff --git a/packages/frontend/.vscode/storybook.code-snippets b/packages/frontend/.vscode/storybook.code-snippets new file mode 100644 index 000000000000..785d0a16081a --- /dev/null +++ b/packages/frontend/.vscode/storybook.code-snippets @@ -0,0 +1,84 @@ +{ + "Storybook Story Impl File": { + "scope": "typescript", + "prefix": "storyimpl", + "body": [ + "/* eslint-disable @typescript-eslint/explicit-function-return-type */", + "import { StoryObj } from '@storybook/vue3';", + "import $1 from './$1.vue';", + "export const Default = {", + "\trender(args) {", + "\t\treturn {", + "\t\t\tcomponents: {", + "\t\t\t\t$1,", + "\t\t\t},", + "\t\t\tsetup() {", + "\t\t\t\treturn {", + "\t\t\t\t\targs,", + "\t\t\t\t};", + "\t\t\t},", + "\t\t\tcomputed: {", + "\t\t\t\tprops() {", + "\t\t\t\t\treturn {", + "\t\t\t\t\t\t...this.args,", + "\t\t\t\t\t};", + "\t\t\t\t},", + "\t\t\t},", + "\t\t\ttemplate: '<$1 v-bind=\"props\" />',", + "\t\t};", + "\t},", + "\targs: {", + "\t\t$2", + "\t},", + "\tparameters: {", + "\t\tlayout: 'centered',", + "\t},", + "} satisfies StoryObj;", + "" + ] + }, + "Storybook Story Impl File (w/ events)": { + "scope": "typescript", + "prefix": "storyimplevent", + "body": [ + "/* eslint-disable @typescript-eslint/explicit-function-return-type */", + "import { action } from '@storybook/addon-actions';", + "import { StoryObj } from '@storybook/vue3';", + "import $1 from './$1.vue';", + "export const Default = {", + "\trender(args) {", + "\t\treturn {", + "\t\t\tcomponents: {", + "\t\t\t\t$1,", + "\t\t\t},", + "\t\t\tsetup() {", + "\t\t\t\treturn {", + "\t\t\t\t\targs,", + "\t\t\t\t};", + "\t\t\t},", + "\t\t\tcomputed: {", + "\t\t\t\tprops() {", + "\t\t\t\t\treturn {", + "\t\t\t\t\t\t...this.args,", + "\t\t\t\t\t};", + "\t\t\t\t},", + "\t\t\t\tevents() {", + "\t\t\t\t\treturn {", + "\t\t\t\t\t\t$3", + "\t\t\t\t\t};", + "\t\t\t\t},", + "\t\t\t},", + "\t\t\ttemplate: '<$1 v-bind=\"props\" v-on=\"events\" />',", + "\t\t};", + "\t},", + "\targs: {", + "\t\t$2", + "\t},", + "\tparameters: {", + "\t\tlayout: 'centered',", + "\t},", + "} satisfies StoryObj;", + "" + ] + } +} From 80161afc140f1bde804c83b3e39424f1a2c70325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Tue, 4 Apr 2023 22:15:17 +0900 Subject: [PATCH 02/12] test(#10336): add `components/Mk[A-B].*` stories --- CONTRIBUTING.md | 1 - packages/frontend/.storybook/fakes.ts | 123 +++++++++-------- packages/frontend/.storybook/generate.tsx | 31 +++-- packages/frontend/.storybook/mocks.ts | 10 ++ packages/frontend/package.json | 37 ++--- .../components/MkAbuseReport.stories.impl.ts | 49 +++++++ .../MkAbuseReportWindow.stories.impl.ts | 49 +++++++ .../components/MkAchievements.stories.impl.ts | 56 ++++++++ .../components/MkAnalogClock.stories.impl.ts | 9 ++ .../frontend/src/components/MkAnalogClock.vue | 12 +- .../src/components/MkAsUi.stories.impl.ts | 2 + .../components/MkAutocomplete.stories.impl.ts | 126 ++++++++++++++++++ .../src/components/MkAvatars.stories.impl.ts | 46 +++++++ .../src/components/MkButton.stories.impl.ts | 53 +++++++- .../components/global/MkAcct.stories.impl.ts | 4 +- .../global/MkAvatar.stories.impl.ts | 4 +- .../components/global/MkError.stories.impl.ts | 36 +++++ .../frontend/src/components/global/MkTime.vue | 3 +- .../global/MkUserName.stories.impl.ts | 8 +- pnpm-lock.yaml | 3 + 20 files changed, 562 insertions(+), 100 deletions(-) create mode 100644 packages/frontend/src/components/MkAbuseReport.stories.impl.ts create mode 100644 packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts create mode 100644 packages/frontend/src/components/MkAchievements.stories.impl.ts create mode 100644 packages/frontend/src/components/MkAsUi.stories.impl.ts create mode 100644 packages/frontend/src/components/MkAutocomplete.stories.impl.ts create mode 100644 packages/frontend/src/components/MkAvatars.stories.impl.ts create mode 100644 packages/frontend/src/components/global/MkError.stories.impl.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fece05d7a940..b8a20c8078cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -245,7 +245,6 @@ You can override the default story by creating a impl story file (`MyComponent.s ```ts /* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-duplicates */ import { StoryObj } from '@storybook/vue3'; import MyComponent from './MyComponent.vue'; export const Default = { diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index b620cf68a308..339f6c079ac1 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -1,54 +1,73 @@ import type { entities } from 'misskey-js' -export const userDetailed = { - id: 'someuserid', - username: 'miskist', - host: 'misskey-hub.net', - name: 'Misskey User', - onlineStatus: 'unknown', - avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', - avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', - emojis: [], - bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog', - bannerColor: '#000000', - bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', - birthday: '2014-06-20', - createdAt: '2016-12-28T22:49:51.000Z', - description: 'I am a cool user!', - ffVisibility: 'public', - fields: [ - { - name: 'Website', - value: 'https://misskey-hub.net', - }, - ], - followersCount: 1024, - followingCount: 16, - hasPendingFollowRequestFromYou: false, - hasPendingFollowRequestToYou: false, - isAdmin: false, - isBlocked: false, - isBlocking: false, - isBot: false, - isCat: false, - isFollowed: false, - isFollowing: false, - isLocked: false, - isModerator: false, - isMuted: false, - isSilenced: false, - isSuspended: false, - lang: 'en', - location: 'Fediverse', - notesCount: 65536, - pinnedNoteIds: [], - pinnedNotes: [], - pinnedPage: null, - pinnedPageId: null, - publicReactions: false, - securityKeys: false, - twoFactorEnabled: false, - updatedAt: null, - uri: null, - url: null, -} satisfies entities.UserDetailed +export function abuseUserReport() { + return { + id: 'someabusereportid', + createdAt: '2016-12-28T22:49:51.000Z', + comment: 'This user is a spammer!', + resolved: false, + reporterId: 'reporterid', + targetUserId: 'targetuserid', + assigneeId: 'assigneeid', + reporter: userDetailed('reporterid', 'reporter', 'misskey-hub.net', 'Reporter'), + targetUser: userDetailed('targetuserid', 'target', 'misskey-hub.net', 'Target'), + assignee: userDetailed('assigneeid', 'assignee', 'misskey-hub.net', 'Assignee'), + me: null, + forwarded: false, + }; +} + +export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'Misskey User'): entities.UserDetailed { + return { + id, + username, + host, + name, + onlineStatus: 'unknown', + avatarUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', + avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', + emojis: [], + bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog', + bannerColor: '#000000', + bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', + birthday: '2014-06-20', + createdAt: '2016-12-28T22:49:51.000Z', + description: 'I am a cool user!', + ffVisibility: 'public', + fields: [ + { + name: 'Website', + value: 'https://misskey-hub.net', + }, + ], + followersCount: 1024, + followingCount: 16, + hasPendingFollowRequestFromYou: false, + hasPendingFollowRequestToYou: false, + isAdmin: false, + isBlocked: false, + isBlocking: false, + isBot: false, + isCat: false, + isFollowed: false, + isFollowing: false, + isLocked: false, + isModerator: false, + isMuted: false, + isSilenced: false, + isSuspended: false, + lang: 'en', + location: 'Fediverse', + notesCount: 65536, + pinnedNoteIds: [], + pinnedNotes: [], + pinnedPage: null, + pinnedPageId: null, + publicReactions: false, + securityKeys: false, + twoFactorEnabled: false, + updatedAt: null, + uri: null, + url: null, + }; +} diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index f0865fcc2492..d2bc276e8aab 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -118,7 +118,7 @@ function toStories(component: string): string { .replace(/[-.]|^(?=\d)/g, '_') .replace(/(?<=^[^A-Z_]*$)/, '_')} /> as estree.Identifier; - const parameters = ( + const parameters = - ) as estree.ObjectExpression; - const program = ( + /> as estree.ObjectExpression; + const program = ) as estree.Identifier} /> as estree.ExportDefaultDeclaration, ]} - /> - ) as estree.Program; + /> as estree.Program; return format( '/* eslint-disable @typescript-eslint/explicit-function-return-type */\n' + '/* eslint-disable import/no-default-export */\n' + + '/* eslint-disable import/no-duplicates */\n' + generate(program, { generator }) + (hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''), { @@ -394,13 +393,13 @@ function toStories(component: string): string { ); } -// glob('src/{components,pages,ui,widgets}/**/*.vue').then( -glob('src/components/global/**/*.vue').then( - (components) => - Promise.all( - components.map((component) => { - const stories = component.replace(/\.vue$/, '.stories.ts'); - return writeFile(stories, toStories(component)); - }) - ) -); +// glob('src/{components,pages,ui,widgets}/**/*.vue') +Promise.all([ + glob('src/components/global/*.vue'), + glob('src/components/Mk{A,B}*.vue'), +]) + .then((globs) => globs.flat()) + .then((components) => Promise.all(components.map((component) => { + const stories = component.replace(/\.vue$/, '.stories.ts'); + return writeFile(stories, toStories(component)); + }))); diff --git a/packages/frontend/.storybook/mocks.ts b/packages/frontend/.storybook/mocks.ts index 41c3c5c4d926..4091e39686fb 100644 --- a/packages/frontend/.storybook/mocks.ts +++ b/packages/frontend/.storybook/mocks.ts @@ -8,6 +8,16 @@ export const onUnhandledRequest = ((req, print) => { }) satisfies SharedOptions['onUnhandledRequest']; export const commonHandlers = [ + rest.get('/fluent-emoji/:codepoints.png', async (req, res, ctx) => { + const { codepoints } = req.params; + const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob()); + return res(ctx.set('Content-Type', 'image/png'), ctx.body(value)); + }), + rest.get('/fluent-emojis/:codepoints.png', async (req, res, ctx) => { + const { codepoints } = req.params; + const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob()); + return res(ctx.set('Content-Type', 'image/png'), ctx.body(value)); + }), rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => { const { codepoints } = req.params; const value = await fetch(`https://unpkg.com/@discordapp/twemoji@14.1.2/dist/svg/${codepoints}.svg`).then((response) => response.blob()); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 2d96d5514eaf..04fc0c251bbd 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -74,23 +74,24 @@ "vuedraggable": "next" }, "devDependencies": { - "@storybook/addon-essentials": "7.0.2", - "@storybook/addon-interactions": "7.0.2", - "@storybook/addon-links": "7.0.2", - "@storybook/addon-storysource": "7.0.2", - "@storybook/addons": "7.0.2", - "@storybook/blocks": "7.0.2", - "@storybook/core-events": "7.0.2", - "@storybook/jest": "0.1.0", - "@storybook/manager-api": "7.0.2", - "@storybook/preview-api": "7.0.2", - "@storybook/react": "7.0.2", - "@storybook/react-vite": "7.0.2", - "@storybook/testing-library": "0.0.14-next.1", - "@storybook/theming": "7.0.2", - "@storybook/types": "7.0.2", - "@storybook/vue3": "7.0.2", - "@storybook/vue3-vite": "7.0.2", + "@storybook/addon-actions": "^7.0.2", + "@storybook/addon-essentials": "^7.0.2", + "@storybook/addon-interactions": "^7.0.2", + "@storybook/addon-links": "^7.0.2", + "@storybook/addon-storysource": "^7.0.2", + "@storybook/addons": "^7.0.2", + "@storybook/blocks": "^7.0.2", + "@storybook/core-events": "^7.0.2", + "@storybook/jest": "~0.1.0", + "@storybook/manager-api": "^7.0.2", + "@storybook/preview-api": "^7.0.2", + "@storybook/react": "^7.0.2", + "@storybook/react-vite": "^7.0.2", + "@storybook/testing-library": "~0.0.14-next.1", + "@storybook/theming": "^7.0.2", + "@storybook/types": "^7.0.2", + "@storybook/vue3": "^7.0.2", + "@storybook/vue3-vite": "^7.0.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/vue": "^6.6.1", "@types/escape-regexp": "0.0.1", @@ -128,7 +129,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "start-server-and-test": "2.0.0", - "storybook": "7.0.2", + "storybook": "^7.0.2", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "summaly": "github:misskey-dev/summaly", "vitest": "^0.29.8", diff --git a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts b/packages/frontend/src/components/MkAbuseReport.stories.impl.ts new file mode 100644 index 000000000000..7d27adeb0448 --- /dev/null +++ b/packages/frontend/src/components/MkAbuseReport.stories.impl.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { abuseUserReport } from '../../.storybook/fakes'; +import { commonHandlers } from '../../.storybook/mocks'; +import MkAbuseReport from './MkAbuseReport.vue'; +export const Default = { + render(args) { + return { + components: { + MkAbuseReport, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + resolved: action('resolved'), + }; + }, + }, + template: '', + }; + }, + args: { + report: abuseUserReport(), + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + rest.post('/api/admin/resolve-abuse-user-report', async (req, res, ctx) => { + action('POST /api/admin/resolve-abuse-user-report')(await req.json()); + return res(ctx.json({})); + }), + ], + }, + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts new file mode 100644 index 000000000000..d0877ffd3be6 --- /dev/null +++ b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { userDetailed } from '../../.storybook/fakes'; +import { commonHandlers } from '../../.storybook/mocks'; +import MkAbuseReportWindow from './MkAbuseReportWindow.vue'; +export const Default = { + render(args) { + return { + components: { + MkAbuseReportWindow, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + 'closed': action('closed'), + }; + }, + }, + template: '', + }; + }, + args: { + user: userDetailed(), + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + rest.post('/api/users/report-abuse', async (req, res, ctx) => { + action('POST /api/users/report-abuse')(await req.json()); + return res(ctx.json({})); + }), + ], + }, + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkAchievements.stories.impl.ts b/packages/frontend/src/components/MkAchievements.stories.impl.ts new file mode 100644 index 000000000000..477152a47b67 --- /dev/null +++ b/packages/frontend/src/components/MkAchievements.stories.impl.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { userDetailed } from '../../.storybook/fakes'; +import { commonHandlers } from '../../.storybook/mocks'; +import MkAchievements from './MkAchievements.vue'; +import { ACHIEVEMENT_TYPES } from '@/scripts/achievements'; +export const Empty = { + render(args) { + return { + components: { + MkAchievements, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '', + }; + }, + args: { + user: userDetailed(), + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + rest.post('/api/users/achievements', (req, res, ctx) => { + return res(ctx.json([])); + }), + ], + }, + }, +} satisfies StoryObj; +export const All = { + ...Empty, + parameters: { + msw: { + handlers: [ + ...commonHandlers, + rest.post('/api/users/achievements', (req, res, ctx) => { + return res(ctx.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 })))); + }), + ], + }, + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts index 05190aa26891..e7fbb472846e 100644 --- a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts +++ b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { StoryObj } from '@storybook/vue3'; import MkAnalogClock from './MkAnalogClock.vue'; +import isChromatic from 'chromatic'; export const Default = { render(args) { return { @@ -22,6 +23,14 @@ export const Default = { template: '', }; }, + args: { + now: isChromatic() ? () => new Date('2023-01-01T10:10:30') : undefined, + }, + decorators: [ + () => ({ + template: '
', + }), + ], parameters: { layout: 'fullscreen', }, diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index 12182026166a..69cda5d03ffc 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -99,6 +99,7 @@ const props = withDefaults(defineProps<{ graduations?: 'none' | 'dots' | 'numbers'; fadeGraduations?: boolean; sAnimation?: 'none' | 'elastic' | 'easeOut'; + now?: () => Date; }>(), { numbers: false, thickness: 0.1, @@ -107,6 +108,7 @@ const props = withDefaults(defineProps<{ graduations: 'dots', fadeGraduations: true, sAnimation: 'elastic', + now: () => new Date(), }); const graduationsMajor = computed(() => { @@ -145,11 +147,17 @@ let disableSAnimate = $ref(false); let sOneRound = false; function tick() { - const now = new Date(); - now.setMinutes(now.getMinutes() + (new Date().getTimezoneOffset() + props.offset)); + const now = props.now(); + now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset); + const previousS = s.value; + const previousM = m.value; + const previousH = h.value; s = now.getSeconds(); m = now.getMinutes(); h = now.getHours(); + if (previousS === s && previousM === m && previousH === h) { + return; + } hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6); mAngle = Math.PI * (m + s / 60) / 30; if (sOneRound) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない) diff --git a/packages/frontend/src/components/MkAsUi.stories.impl.ts b/packages/frontend/src/components/MkAsUi.stories.impl.ts new file mode 100644 index 000000000000..b67c0e679d58 --- /dev/null +++ b/packages/frontend/src/components/MkAsUi.stories.impl.ts @@ -0,0 +1,2 @@ +import MkAsUi from './MkAsUi.vue'; +void MkAsUi; diff --git a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts new file mode 100644 index 000000000000..1064853e89ec --- /dev/null +++ b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { userDetailed } from '../../.storybook/fakes'; +import { commonHandlers } from '../../.storybook/mocks'; +import MkAutocomplete from './MkAutocomplete.vue'; +import MkInput from './MkInput.vue'; +const common = { + render(args) { + return { + components: { + MkAutocomplete, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + open: action('open'), + closed: action('closed'), + }; + }, + }, + template: '', + }; + }, + args: { + close: action('close'), + x: 0, + y: 0, + }, + decorators: [ + (_, context) => ({ + components: { + MkInput, + }, + data() { + return { + q: context.args.q, + textarea: null, + }; + }, + methods: { + inputMounted() { + this.textarea = this.$refs.input.$refs.inputEl; + }, + }, + template: '', + }), + ], + parameters: { + controls: { + exclude: ['textarea'], + }, + layout: 'centered', + }, +} satisfies StoryObj; +export const User = { + ...common, + args: { + ...common.args, + type: 'user', + q: 'm', + }, + parameters: { + ...common.parameters, + msw: { + handlers: [ + ...commonHandlers, + rest.post('/api/users/search-by-username-and-host', (req, res, ctx) => { + return res(ctx.json([ + userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'), + userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'), + ])); + }), + ], + }, + }, +}; +export const Hashtag = { + ...common, + args: { + ...common.args, + type: 'hashtag', + q: '気象', + }, + parameters: { + ...common.parameters, + msw: { + handlers: [ + ...commonHandlers, + rest.post('/api/hashtags/search', (req, res, ctx) => { + return res(ctx.json([ + '気象警報注意報', + '気象警報', + '気象情報', + ])); + }), + ], + }, + }, +}; +export const Emoji = { + ...common, + args: { + ...common.args, + type: 'emoji', + q: 'smile', + }, +} satisfies StoryObj; +export const MfmTag = { + ...common, + args: { + ...common.args, + type: 'mfmTag', + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkAvatars.stories.impl.ts b/packages/frontend/src/components/MkAvatars.stories.impl.ts new file mode 100644 index 000000000000..14052c7343b4 --- /dev/null +++ b/packages/frontend/src/components/MkAvatars.stories.impl.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { StoryObj } from '@storybook/vue3'; +import { rest } from 'msw'; +import { userDetailed } from '../../.storybook/fakes'; +import { commonHandlers } from '../../.storybook/mocks'; +import MkAvatars from './MkAvatars.vue'; +export const Default = { + render(args) { + return { + components: { + MkAvatars, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '', + }; + }, + args: { + userIds: ['17', '20', '18'], + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + rest.post('/api/users/show', (req, res, ctx) => { + return res(ctx.json([ + userDetailed('17'), + userDetailed('20'), + userDetailed('18'), + ])); + }), + ], + }, + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkButton.stories.impl.ts b/packages/frontend/src/components/MkButton.stories.impl.ts index e1c1c54d10f0..982a8b3be126 100644 --- a/packages/frontend/src/components/MkButton.stories.impl.ts +++ b/packages/frontend/src/components/MkButton.stories.impl.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable import/no-default-export */ -/* eslint-disable import/no-duplicates */ +import { action } from '@storybook/addon-actions'; import { StoryObj } from '@storybook/vue3'; import MkButton from './MkButton.vue'; export const Default = { @@ -20,11 +20,60 @@ export const Default = { ...this.args, }; }, + events() { + return { + click: action('click'), + }; + }, }, - template: 'Text', + template: 'Text', }; }, + args: { + }, parameters: { layout: 'centered', }, } satisfies StoryObj; +export const Primary = { + ...Default, + args: { + ...Default.args, + primary: true, + }, +} satisfies StoryObj; +export const Gradate = { + ...Default, + args: { + ...Default.args, + gradate: true, + }, +} satisfies StoryObj; +export const Rounded = { + ...Default, + args: { + ...Default.args, + rounded: true, + }, +} satisfies StoryObj; +export const Danger = { + ...Default, + args: { + ...Default.args, + danger: true, + }, +} satisfies StoryObj; +export const Small = { + ...Default, + args: { + ...Default.args, + small: true, + }, +} satisfies StoryObj; +export const Large = { + ...Default, + args: { + ...Default.args, + large: true, + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/global/MkAcct.stories.impl.ts b/packages/frontend/src/components/global/MkAcct.stories.impl.ts index 7dfa1a14f2cd..d5e3fc356829 100644 --- a/packages/frontend/src/components/global/MkAcct.stories.impl.ts +++ b/packages/frontend/src/components/global/MkAcct.stories.impl.ts @@ -25,7 +25,7 @@ export const Default = { }, args: { user: { - ...userDetailed, + ...userDetailed(), host: null, }, }, @@ -37,7 +37,7 @@ export const Detail = { ...Default, args: { ...Default.args, - user: userDetailed, + user: userDetailed(), detail: true, }, } satisfies StoryObj; diff --git a/packages/frontend/src/components/global/MkAvatar.stories.impl.ts b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts index 6c46f75b5f0d..3c69c808257e 100644 --- a/packages/frontend/src/components/global/MkAvatar.stories.impl.ts +++ b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts @@ -24,7 +24,7 @@ const common = { }; }, args: { - user: userDetailed, + user: userDetailed(), }, decorators: [ (Story, context) => ({ @@ -49,7 +49,7 @@ export const ProfilePageCat = { args: { ...ProfilePage.args, user: { - ...userDetailed, + ...userDetailed(), isCat: true, }, }, diff --git a/packages/frontend/src/components/global/MkError.stories.impl.ts b/packages/frontend/src/components/global/MkError.stories.impl.ts new file mode 100644 index 000000000000..0a58edb3bbea --- /dev/null +++ b/packages/frontend/src/components/global/MkError.stories.impl.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import MkError from './MkError.vue'; +export const Default = { + render(args) { + return { + components: { + MkError, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + retry: action('retry'), + }; + }, + }, + template: '', + }; + }, + args: { + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 99169512dbd7..261cc0ee18ca 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -8,6 +8,7 @@ diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 8c3478d8f226..69fb4b93be7d 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -7,7 +7,7 @@
- +
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca38809686ac..2e6ed96e13aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -935,7 +935,7 @@ importers: specifier: 2.0.0 version: 2.0.0 storybook: - specifier: ^7.0.2 + specifier: 7.0.2 version: 7.0.2 storybook-addon-misskey-theme: specifier: github:misskey-dev/storybook-addon-misskey-theme From e649b1b1e6771bee674f2dfb044e0efd72d0be5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Sun, 9 Apr 2023 04:40:47 +0000 Subject: [PATCH 09/12] refactor: avoid temporary previous tapping declarations --- packages/frontend/src/components/MkAnalogClock.vue | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index f12020f81047..dc584eda0d95 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -149,13 +149,7 @@ let sOneRound = false; function tick() { const now = props.now(); now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset); - const previousS = s; - const previousM = m; - const previousH = h; - s = now.getSeconds(); - m = now.getMinutes(); - h = now.getHours(); - if (previousS === s && previousM === m && previousH === h) { + if (s === (s = now.getSeconds()) && m === (m = now.getMinutes()) && h === (h = now.getHours())) { return; } hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6); From 535e910419d1d083df776535885c5ef421015048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Sun, 9 Apr 2023 13:51:41 +0900 Subject: [PATCH 10/12] revert: avoid temporary previous tapping declarations This reverts commit e649b1b1e6771bee674f2dfb044e0efd72d0be5d. --- packages/frontend/src/components/MkAnalogClock.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue index dc584eda0d95..f12020f81047 100644 --- a/packages/frontend/src/components/MkAnalogClock.vue +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -149,7 +149,13 @@ let sOneRound = false; function tick() { const now = props.now(); now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset); - if (s === (s = now.getSeconds()) && m === (m = now.getMinutes()) && h === (h = now.getHours())) { + const previousS = s; + const previousM = m; + const previousH = h; + s = now.getSeconds(); + m = now.getMinutes(); + h = now.getHours(); + if (previousS === s && previousM === m && previousH === h) { return; } hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6); From a6bdb245f7bcc65c4a7e695f55abb849aeeadecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Tue, 11 Apr 2023 02:39:40 +0900 Subject: [PATCH 11/12] test: flaky snapshots --- .../components/MkAutocomplete.stories.impl.ts | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts index 1064853e89ec..bb2e89ddecc3 100644 --- a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts +++ b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts @@ -1,7 +1,10 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { action } from '@storybook/addon-actions'; +import { expect } from '@storybook/jest'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; import { StoryObj } from '@storybook/vue3'; import { rest } from 'msw'; +import { tick } from '@/scripts/test-utils'; import { userDetailed } from '../../.storybook/fakes'; import { commonHandlers } from '../../.storybook/mocks'; import MkAutocomplete from './MkAutocomplete.vue'; @@ -62,6 +65,10 @@ const common = { exclude: ['textarea'], }, layout: 'centered', + chromatic: { + // FIXME: flaky + disableSnapshot: true, + }, }, } satisfies StoryObj; export const User = { @@ -69,7 +76,18 @@ export const User = { args: { ...common.args, type: 'user', - q: 'm', + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const input = canvas.getByRole('combobox'); + await waitFor(() => userEvent.hover(input)); + await waitFor(() => userEvent.click(input)); + await waitFor(() => userEvent.type(input, 'm')); + await waitFor(async () => { + await userEvent.type(input, ' ', { delay: 256 }); + await tick(); + return await expect(canvas.getByRole('list')).toBeInTheDocument(); + }, { timeout: 16384 }); }, parameters: { ...common.parameters, @@ -91,7 +109,18 @@ export const Hashtag = { args: { ...common.args, type: 'hashtag', - q: '気象', + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const input = canvas.getByRole('combobox'); + await waitFor(() => userEvent.hover(input)); + await waitFor(() => userEvent.click(input)); + await waitFor(() => userEvent.type(input, '気象')); + await waitFor(async () => { + await userEvent.type(input, ' ', { delay: 256 }); + await tick(); + return await expect(canvas.getByRole('list')).toBeInTheDocument(); + }, { interval: 256, timeout: 16384 }); }, parameters: { ...common.parameters, @@ -114,7 +143,18 @@ export const Emoji = { args: { ...common.args, type: 'emoji', - q: 'smile', + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const input = canvas.getByRole('combobox'); + await waitFor(() => userEvent.hover(input)); + await waitFor(() => userEvent.click(input)); + await waitFor(() => userEvent.type(input, 'smile')); + await waitFor(async () => { + await userEvent.type(input, ' ', { delay: 256 }); + await tick(); + return await expect(canvas.getByRole('list')).toBeInTheDocument(); + }, { interval: 256, timeout: 16384 }); }, } satisfies StoryObj; export const MfmTag = { @@ -123,4 +163,14 @@ export const MfmTag = { ...common.args, type: 'mfmTag', }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const input = canvas.getByRole('combobox'); + await waitFor(() => userEvent.hover(input)); + await waitFor(() => userEvent.click(input)); + await waitFor(async () => { + await tick(); + return await expect(canvas.getByRole('list')).toBeInTheDocument(); + }, { interval: 256, timeout: 16384 }); + }, } satisfies StoryObj; From 7a3216213734f8cddfa824a405c53fdccf9a4efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Tue, 11 Apr 2023 02:43:08 +0900 Subject: [PATCH 12/12] style: import --- packages/frontend/src/components/MkAutocomplete.stories.impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts index bb2e89ddecc3..075904d6a356 100644 --- a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts +++ b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts @@ -4,11 +4,11 @@ import { expect } from '@storybook/jest'; import { userEvent, waitFor, within } from '@storybook/testing-library'; import { StoryObj } from '@storybook/vue3'; import { rest } from 'msw'; -import { tick } from '@/scripts/test-utils'; import { userDetailed } from '../../.storybook/fakes'; import { commonHandlers } from '../../.storybook/mocks'; import MkAutocomplete from './MkAutocomplete.vue'; import MkInput from './MkInput.vue'; +import { tick } from '@/scripts/test-utils'; const common = { render(args) { return {