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:
		
							parent
							
								
									a6edd50a5d
								
							
						
					
					
						commit
						f1b1e2a7cc
					
				
					 7 changed files with 41 additions and 92 deletions
				
			
		| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 * 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 { type Preview, setup } from '@storybook/vue3';
 | 
			
		||||
import isChromatic from 'chromatic/isChromatic';
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ import '../src/style.scss';
 | 
			
		|||
 | 
			
		||||
const appInitialized = Symbol();
 | 
			
		||||
 | 
			
		||||
let lastStory = null;
 | 
			
		||||
let lastStory: string | null = null;
 | 
			
		||||
let moduleInitialized = false;
 | 
			
		||||
let unobserve = () => {};
 | 
			
		||||
let misskeyOS = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +110,7 @@ const preview = {
 | 
			
		|||
				}).catch(() => {});
 | 
			
		||||
				Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
 | 
			
		||||
					initLocalStorage();
 | 
			
		||||
					channel.emit(FORCE_REMOUNT, { storyId: context.id });
 | 
			
		||||
					channel.emit(FORCE_RE_RENDER, { storyId: context.id });
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
			const story = Story();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,14 +12,12 @@ import { expect, userEvent, within } from '@storybook/test';
 | 
			
		|||
import { channel } from '../../.storybook/fakes.js';
 | 
			
		||||
import { commonHandlers } from '../../.storybook/mocks.js';
 | 
			
		||||
import MkChannelFollowButton from './MkChannelFollowButton.vue';
 | 
			
		||||
import { semaphore } from '@/scripts/test-utils.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
function sleep(ms: number) {
 | 
			
		||||
	return new Promise(resolve => setTimeout(resolve, ms));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const s = semaphore();
 | 
			
		||||
export const Default = {
 | 
			
		||||
	render(args) {
 | 
			
		||||
		return {
 | 
			
		||||
| 
						 | 
				
			
			@ -46,17 +44,13 @@ export const Default = {
 | 
			
		|||
		full: true,
 | 
			
		||||
	},
 | 
			
		||||
	async play({ canvasElement }) {
 | 
			
		||||
		await s.acquire();
 | 
			
		||||
		await sleep(1000);
 | 
			
		||||
		const canvas = within(canvasElement);
 | 
			
		||||
		const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
 | 
			
		||||
		await expect(buttonElement).toHaveTextContent(i18n.ts.follow);
 | 
			
		||||
		await userEvent.click(buttonElement);
 | 
			
		||||
		await sleep(1000);
 | 
			
		||||
		await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow);
 | 
			
		||||
		await sleep(100);
 | 
			
		||||
		await userEvent.click(buttonElement);
 | 
			
		||||
		s.release();
 | 
			
		||||
	},
 | 
			
		||||
	parameters: {
 | 
			
		||||
		layout: 'centered',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@
 | 
			
		|||
import { StoryObj } from '@storybook/vue3';
 | 
			
		||||
import { HttpResponse, http } from 'msw';
 | 
			
		||||
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 MkClickerGame from './MkClickerGame.vue';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,12 +41,10 @@ export const Default = {
 | 
			
		|||
		await sleep(1000);
 | 
			
		||||
		const canvas = within(canvasElement);
 | 
			
		||||
		const count = canvas.getByTestId('count');
 | 
			
		||||
		// NOTE: flaky なので N/A も通しておく
 | 
			
		||||
		await expect(count).toHaveTextContent(/^(0|N\/A)$/);
 | 
			
		||||
		// FIXME: flaky
 | 
			
		||||
		// const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
 | 
			
		||||
		// await userEvent.click(buttonElement);
 | 
			
		||||
		// await expect(count).toHaveTextContent('1');
 | 
			
		||||
		await expect(count).toHaveTextContent('0');
 | 
			
		||||
		const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
 | 
			
		||||
		await userEvent.click(buttonElement);
 | 
			
		||||
		await expect(count).toHaveTextContent('1');
 | 
			
		||||
	},
 | 
			
		||||
	parameters: {
 | 
			
		||||
		layout: 'centered',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,13 +11,6 @@ import { expect, userEvent, within } from '@storybook/test';
 | 
			
		|||
import { file } from '../../.storybook/fakes.js';
 | 
			
		||||
import MkCwButton from './MkCwButton.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { semaphore } from '@/scripts/test-utils.js';
 | 
			
		||||
 | 
			
		||||
function sleep(ms: number) {
 | 
			
		||||
	return new Promise(resolve => setTimeout(resolve, ms));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const s = semaphore();
 | 
			
		||||
 | 
			
		||||
export const Default = {
 | 
			
		||||
	render(args) {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,8 +47,6 @@ export const Default = {
 | 
			
		|||
		text: 'Some CW content',
 | 
			
		||||
	},
 | 
			
		||||
	async play({ canvasElement }) {
 | 
			
		||||
		await s.acquire();
 | 
			
		||||
		await sleep(1000);
 | 
			
		||||
		const canvas = within(canvasElement);
 | 
			
		||||
		const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
 | 
			
		||||
		await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show);
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +54,6 @@ export const Default = {
 | 
			
		|||
		await userEvent.click(buttonElement);
 | 
			
		||||
		await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide);
 | 
			
		||||
		await userEvent.click(buttonElement);
 | 
			
		||||
		s.release();
 | 
			
		||||
	},
 | 
			
		||||
	parameters: {
 | 
			
		||||
		chromatic: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,12 +35,10 @@ export const Default = {
 | 
			
		|||
		// FIXME: 通るけどその後落ちるのでコメントアウト
 | 
			
		||||
		// await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
 | 
			
		||||
		await userEvent.pointer({ keys: '[MouseRight]', target: a });
 | 
			
		||||
		await tick();
 | 
			
		||||
		const menu = canvas.getByRole('menu');
 | 
			
		||||
		await expect(menu).toBeInTheDocument();
 | 
			
		||||
		await userEvent.click(a);
 | 
			
		||||
		a.blur();
 | 
			
		||||
		await tick();
 | 
			
		||||
		await expect(menu).not.toBeInTheDocument();
 | 
			
		||||
	},
 | 
			
		||||
	args: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,12 +9,6 @@ import { StoryObj } from '@storybook/vue3';
 | 
			
		|||
import MkAd from './MkAd.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
let lock: Promise<undefined> | undefined;
 | 
			
		||||
 | 
			
		||||
function sleep(ms: number) {
 | 
			
		||||
	return new Promise(resolve => setTimeout(resolve, ms));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const common = {
 | 
			
		||||
	render(args) {
 | 
			
		||||
		return {
 | 
			
		||||
| 
						 | 
				
			
			@ -37,56 +31,41 @@ const common = {
 | 
			
		|||
		};
 | 
			
		||||
	},
 | 
			
		||||
	async play({ canvasElement, args }) {
 | 
			
		||||
		if (lock) {
 | 
			
		||||
			console.warn('This test is unexpectedly running twice in parallel, fix it!');
 | 
			
		||||
			console.warn('See also: https://github.com/misskey-dev/misskey/issues/11267');
 | 
			
		||||
			await lock;
 | 
			
		||||
		const canvas = within(canvasElement);
 | 
			
		||||
		const a = canvas.getByRole<HTMLAnchorElement>('link');
 | 
			
		||||
		// FIXME: 通るけどその後落ちるのでコメントアウト
 | 
			
		||||
		// 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);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let resolve: (value?: any) => void;
 | 
			
		||||
		lock = new Promise(r => resolve = r);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			// NOTE: sleep しないと何故か落ちる
 | 
			
		||||
			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).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();
 | 
			
		||||
	},
 | 
			
		||||
	args: {
 | 
			
		||||
		prefer: [],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,13 +7,3 @@ export async function tick(): Promise<void> {
 | 
			
		|||
	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
	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()?.(),
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue