test(#10336): add components/Mk[A-B].* stories (#10475)

* chore(#10336): register snippets

* test(#10336): add `components/Mk[A-B].*` stories

* build: desynced lockfile

* ci(#10336): preload assets

* ci(#10336): use pull_request

* build: update lockfile

* fix: reactivity transform

* chore: track upstream changes

* refactor: avoid temporary previous tapping declarations

* revert: avoid temporary previous tapping declarations

This reverts commit e649b1b1e6.

* test: flaky snapshots

* style: import
This commit is contained in:
Acid Chicken (硫酸鶏)
2023-04-13 12:20:39 +09:00
committed by GitHub
parent 2a7ba37996
commit 9bb6c536c0
21 changed files with 617 additions and 245 deletions

View File

@@ -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: '<MkAbuseReport v-bind="props" v-on="events" />',
};
},
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<typeof MkAbuseReport>;

View File

@@ -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: '<MkAbuseReportWindow v-bind="props" v-on="events" />',
};
},
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<typeof MkAbuseReportWindow>;

View File

@@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { userDetailed } from '../../.storybook/fakes';
import MkAccountMoved from './MkAccountMoved.vue';
export const Default = {
render(args) {
return {
components: {
MkAccountMoved,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkAccountMoved v-bind="props" />',
};
},
args: {
username: userDetailed().username,
host: userDetailed().host,
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkAccountMoved>;

View File

@@ -2,7 +2,7 @@
<div :class="$style.root">
<i class="ti ti-plane-departure" style="margin-right: 8px;"></i>
{{ i18n.ts.accountMoved }}
<MkMention :class="$style.link" :username="acct" :host="host ?? localHost"/>
<MkMention :class="$style.link" :username="username" :host="host ?? localHost"/>
</div>
</template>
@@ -12,7 +12,7 @@ import { i18n } from '@/i18n';
import { host as localHost } from '@/config';
defineProps<{
acct: string;
username: string;
host: string;
}>();
</script>

View File

@@ -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: '<MkAchievements v-bind="props" />',
};
},
args: {
user: userDetailed(),
},
parameters: {
layout: 'fullscreen',
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/achievements', (req, res, ctx) => {
return res(ctx.json([]));
}),
],
},
},
} satisfies StoryObj<typeof MkAchievements>;
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<typeof MkAchievements>;

View File

@@ -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: '<MkAnalogClock v-bind="props" />',
};
},
args: {
now: isChromatic() ? () => new Date('2023-01-01T10:10:30') : undefined,
},
decorators: [
() => ({
template: '<div style="container-type:inline-size;height:100%"><div style="height:100cqmin;margin:auto;width:100cqmin"><story/></div></div>',
}),
],
parameters: {
layout: 'fullscreen',
},

View File

@@ -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;
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);
mAngle = Math.PI * (m + s / 60) / 30;
if (sOneRound) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)

View File

@@ -0,0 +1,2 @@
import MkAsUi from './MkAsUi.vue';
void MkAsUi;

View File

@@ -0,0 +1,176 @@
/* 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 { 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 {
components: {
MkAutocomplete,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
open: action('open'),
closed: action('closed'),
};
},
},
template: '<MkAutocomplete v-bind="props" v-on="events" :textarea="textarea" />',
};
},
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: '<MkInput v-model="q" ref="input" @vue:mounted="inputMounted"/><story v-if="textarea" :q="q" :textarea="textarea"/>',
}),
],
parameters: {
controls: {
exclude: ['textarea'],
},
layout: 'centered',
chromatic: {
// FIXME: flaky
disableSnapshot: true,
},
},
} satisfies StoryObj<typeof MkAutocomplete>;
export const User = {
...common,
args: {
...common.args,
type: 'user',
},
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,
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',
},
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,
msw: {
handlers: [
...commonHandlers,
rest.post('/api/hashtags/search', (req, res, ctx) => {
return res(ctx.json([
'気象警報注意報',
'気象警報',
'気象情報',
]));
}),
],
},
},
};
export const Emoji = {
...common,
args: {
...common.args,
type: 'emoji',
},
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<typeof MkAutocomplete>;
export const MfmTag = {
...common,
args: {
...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<typeof MkAutocomplete>;

View File

@@ -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: '<MkAvatars v-bind="props" />',
};
},
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<typeof MkAvatars>;

View File

@@ -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: '<MkButton v-bind="props">Text</MkButton>',
template: '<MkButton v-bind="props" v-on="events">Text</MkButton>',
};
},
args: {
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkButton>;
export const Primary = {
...Default,
args: {
...Default.args,
primary: true,
},
} satisfies StoryObj<typeof MkButton>;
export const Gradate = {
...Default,
args: {
...Default.args,
gradate: true,
},
} satisfies StoryObj<typeof MkButton>;
export const Rounded = {
...Default,
args: {
...Default.args,
rounded: true,
},
} satisfies StoryObj<typeof MkButton>;
export const Danger = {
...Default,
args: {
...Default.args,
danger: true,
},
} satisfies StoryObj<typeof MkButton>;
export const Small = {
...Default,
args: {
...Default.args,
small: true,
},
} satisfies StoryObj<typeof MkButton>;
export const Large = {
...Default,
args: {
...Default.args,
large: true,
},
} satisfies StoryObj<typeof MkButton>;

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { expect } from '@storybook/jest';
import { waitFor } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
@@ -20,14 +21,21 @@ export const Default = {
...this.args,
};
},
events() {
return {
retry: action('retry'),
};
},
},
template: '<MkError v-bind="props" />',
template: '<MkError v-bind="props" v-on="events" />',
};
},
async play({ canvasElement }) {
await expect(canvasElement.firstElementChild).not.toBeNull();
await waitFor(async () => expect(canvasElement.firstElementChild?.classList).not.toContain('_transition_zoom-enter-active'));
},
args: {
},
parameters: {
layout: 'centered',
},

View File

@@ -8,6 +8,7 @@
</template>
<script lang="ts" setup>
import isChromatic from 'chromatic/isChromatic';
import { onUnmounted } from 'vue';
import { i18n } from '@/i18n';
import { dateTimeFormat } from '@/scripts/intl-const';
@@ -17,7 +18,7 @@ const props = withDefaults(defineProps<{
origin?: Date | null;
mode?: 'relative' | 'absolute' | 'detail';
}>(), {
origin: null,
origin: isChromatic() ? new Date('2023-04-01T00:00:00Z') : null,
mode: 'relative',
});

View File

@@ -7,7 +7,7 @@
<!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
<div class="profile _gaps">
<MkAccountMoved v-if="user.movedToUri" :host="user.movedToUri.host" :acct="user.movedToUri.username"/>
<MkAccountMoved v-if="user.movedToUri" :host="user.movedToUri.host" :username="user.movedToUri.username"/>
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
<div :key="user.id" class="main _panel">