fix(storybook): prevent infinite remount of component (#14101)

* fix(storybook): prevent infinite remount of component

* fix: disable flaky `.toMatch()` test
This commit is contained in:
zyoshoka 2024-07-02 10:57:20 +09:00 committed by GitHub
parent a6edd50a5d
commit f1b1e2a7cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 41 additions and 92 deletions

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { FORCE_REMOUNT } from '@storybook/core-events'; import { FORCE_RE_RENDER, FORCE_REMOUNT } from '@storybook/core-events';
import { addons } from '@storybook/preview-api'; import { addons } from '@storybook/preview-api';
import { type Preview, setup } from '@storybook/vue3'; import { type Preview, setup } from '@storybook/vue3';
import isChromatic from 'chromatic/isChromatic'; import isChromatic from 'chromatic/isChromatic';
@ -16,7 +16,7 @@ import '../src/style.scss';
const appInitialized = Symbol(); const appInitialized = Symbol();
let lastStory = null; let lastStory: string | null = null;
let moduleInitialized = false; let moduleInitialized = false;
let unobserve = () => {}; let unobserve = () => {};
let misskeyOS = null; let misskeyOS = null;
@ -110,7 +110,7 @@ const preview = {
}).catch(() => {}); }).catch(() => {});
Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => { Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
initLocalStorage(); initLocalStorage();
channel.emit(FORCE_REMOUNT, { storyId: context.id }); channel.emit(FORCE_RE_RENDER, { storyId: context.id });
}); });
} }
const story = Story(); const story = Story();

View File

@ -12,14 +12,12 @@ import { expect, userEvent, within } from '@storybook/test';
import { channel } from '../../.storybook/fakes.js'; import { channel } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';
import MkChannelFollowButton from './MkChannelFollowButton.vue'; import MkChannelFollowButton from './MkChannelFollowButton.vue';
import { semaphore } from '@/scripts/test-utils.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
function sleep(ms: number) { function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
const s = semaphore();
export const Default = { export const Default = {
render(args) { render(args) {
return { return {
@ -46,17 +44,13 @@ export const Default = {
full: true, full: true,
}, },
async play({ canvasElement }) { async play({ canvasElement }) {
await s.acquire();
await sleep(1000);
const canvas = within(canvasElement); const canvas = within(canvasElement);
const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
await expect(buttonElement).toHaveTextContent(i18n.ts.follow); await expect(buttonElement).toHaveTextContent(i18n.ts.follow);
await userEvent.click(buttonElement); await userEvent.click(buttonElement);
await sleep(1000); await sleep(1000);
await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow); await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow);
await sleep(100);
await userEvent.click(buttonElement); await userEvent.click(buttonElement);
s.release();
}, },
parameters: { parameters: {
layout: 'centered', layout: 'centered',

View File

@ -8,7 +8,7 @@
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { expect, within } from '@storybook/test'; import { expect, userEvent, within } from '@storybook/test';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';
import MkClickerGame from './MkClickerGame.vue'; import MkClickerGame from './MkClickerGame.vue';
@ -41,12 +41,10 @@ export const Default = {
await sleep(1000); await sleep(1000);
const canvas = within(canvasElement); const canvas = within(canvasElement);
const count = canvas.getByTestId('count'); const count = canvas.getByTestId('count');
// NOTE: flaky なので N/A も通しておく await expect(count).toHaveTextContent('0');
await expect(count).toHaveTextContent(/^(0|N\/A)$/); const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
// FIXME: flaky await userEvent.click(buttonElement);
// const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); await expect(count).toHaveTextContent('1');
// await userEvent.click(buttonElement);
// await expect(count).toHaveTextContent('1');
}, },
parameters: { parameters: {
layout: 'centered', layout: 'centered',

View File

@ -11,13 +11,6 @@ import { expect, userEvent, within } from '@storybook/test';
import { file } from '../../.storybook/fakes.js'; import { file } from '../../.storybook/fakes.js';
import MkCwButton from './MkCwButton.vue'; import MkCwButton from './MkCwButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { semaphore } from '@/scripts/test-utils.js';
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const s = semaphore();
export const Default = { export const Default = {
render(args) { render(args) {
@ -54,8 +47,6 @@ export const Default = {
text: 'Some CW content', text: 'Some CW content',
}, },
async play({ canvasElement }) { async play({ canvasElement }) {
await s.acquire();
await sleep(1000);
const canvas = within(canvasElement); const canvas = within(canvasElement);
const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show); await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show);
@ -63,7 +54,6 @@ export const Default = {
await userEvent.click(buttonElement); await userEvent.click(buttonElement);
await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide); await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide);
await userEvent.click(buttonElement); await userEvent.click(buttonElement);
s.release();
}, },
parameters: { parameters: {
chromatic: { chromatic: {

View File

@ -35,12 +35,10 @@ export const Default = {
// FIXME: 通るけどその後落ちるのでコメントアウト // FIXME: 通るけどその後落ちるのでコメントアウト
// await expect(a.href).toMatch(/^https?:\/\/.*#test$/); // await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
await userEvent.pointer({ keys: '[MouseRight]', target: a }); await userEvent.pointer({ keys: '[MouseRight]', target: a });
await tick();
const menu = canvas.getByRole('menu'); const menu = canvas.getByRole('menu');
await expect(menu).toBeInTheDocument(); await expect(menu).toBeInTheDocument();
await userEvent.click(a); await userEvent.click(a);
a.blur(); a.blur();
await tick();
await expect(menu).not.toBeInTheDocument(); await expect(menu).not.toBeInTheDocument();
}, },
args: { args: {

View File

@ -9,12 +9,6 @@ import { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue'; import MkAd from './MkAd.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
let lock: Promise<undefined> | undefined;
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const common = { const common = {
render(args) { render(args) {
return { return {
@ -37,56 +31,41 @@ const common = {
}; };
}, },
async play({ canvasElement, args }) { async play({ canvasElement, args }) {
if (lock) { const canvas = within(canvasElement);
console.warn('This test is unexpectedly running twice in parallel, fix it!'); const a = canvas.getByRole<HTMLAnchorElement>('link');
console.warn('See also: https://github.com/misskey-dev/misskey/issues/11267'); // FIXME: 通るけどその後落ちるのでコメントアウト
await lock; // 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(canvasElement).toHaveTextContent(i18n.ts._ad.back);
await expect(a).not.toBeInTheDocument();
await expect(i).not.toBeInTheDocument();
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
const hasReduceFrequency = args.specify?.ratio !== 0;
await expect(buttons).toHaveLength(hasReduceFrequency ? 2 : 1);
const reduce = hasReduceFrequency ? buttons[0] : null;
const back = buttons[hasReduceFrequency ? 1 : 0];
if (reduce) {
await expect(reduce).toBeInTheDocument();
await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd);
} }
await expect(back).toBeInTheDocument();
let resolve: (value?: any) => void; await expect(back).toHaveTextContent(i18n.ts._ad.back);
lock = new Promise(r => resolve = r); await userEvent.click(back);
await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
try { if (reduce) {
// NOTE: sleep しないと何故か落ちる await expect(reduce).not.toBeInTheDocument();
await sleep(100);
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(canvasElement).toHaveTextContent(i18n.ts._ad.back);
await expect(a).not.toBeInTheDocument();
await expect(i).not.toBeInTheDocument();
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
const hasReduceFrequency = args.specify?.ratio !== 0;
await expect(buttons).toHaveLength(hasReduceFrequency ? 2 : 1);
const reduce = hasReduceFrequency ? buttons[0] : null;
const back = buttons[hasReduceFrequency ? 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);
await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
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();
} finally {
resolve!();
lock = undefined;
} }
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: { args: {
prefer: [], prefer: [],

View File

@ -7,13 +7,3 @@ export async function tick(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never); await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never);
} }
/**
* @see https://github.com/misskey-dev/misskey/issues/11267
*/
export function semaphore(counter = 0, waiting: (() => void)[] = []) {
return {
acquire: () => ++counter > 1 && new Promise<void>(resolve => waiting.push(resolve)),
release: () => --counter && waiting.pop()?.(),
};
}