feat: 平台错误与完成弹窗收口
This commit is contained in:
@@ -429,6 +429,10 @@ import {
|
||||
PlatformErrorDialog,
|
||||
type PlatformErrorDialogPayload,
|
||||
} from './PlatformErrorDialog';
|
||||
import {
|
||||
PlatformTaskCompletionDialog,
|
||||
type PlatformTaskCompletionDialogPayload,
|
||||
} from './PlatformTaskCompletionDialog';
|
||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
|
||||
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
|
||||
@@ -444,6 +448,7 @@ type DraftGenerationNoticeStatus = 'generating' | 'ready';
|
||||
type DraftGenerationNotice = {
|
||||
status: DraftGenerationNoticeStatus;
|
||||
seen: boolean;
|
||||
completedAtMs?: number;
|
||||
};
|
||||
type DraftGenerationNoticeMap = Record<string, DraftGenerationNotice>;
|
||||
type CreationWorkShelfKind = CreationWorkShelfItem['kind'];
|
||||
@@ -2027,12 +2032,74 @@ function formatPlatformErrorSource(label: string, id?: string | null) {
|
||||
return normalizedId ? `${label} ${normalizedId}` : label;
|
||||
}
|
||||
|
||||
function formatPlatformTaskCompletionSource(label: string, id?: string | null) {
|
||||
const normalizedId = id?.trim();
|
||||
return normalizedId ? `${label} ${normalizedId}` : label;
|
||||
}
|
||||
|
||||
function buildPlatformErrorDialogDismissKey(
|
||||
error: (PlatformErrorDialogPayload & { key: string }) | null,
|
||||
) {
|
||||
return error ? `${error.key}:${error.source}:${error.message}` : null;
|
||||
}
|
||||
|
||||
function buildPlatformTaskCompletionDialogDismissKey(
|
||||
completion:
|
||||
| (PlatformTaskCompletionDialogPayload & {
|
||||
key: string;
|
||||
completedAtMs: number | null;
|
||||
})
|
||||
| null,
|
||||
) {
|
||||
return completion
|
||||
? `${completion.key}:${completion.source}:${completion.message}:${completion.completedAtMs ?? 0}`
|
||||
: null;
|
||||
}
|
||||
|
||||
function pickDraftCompletionDialogSourceId(
|
||||
ids: Array<string | null | undefined>,
|
||||
) {
|
||||
const normalizedIds = ids
|
||||
.map((id) => id?.trim() ?? '')
|
||||
.filter((id) => Boolean(id));
|
||||
return (
|
||||
normalizedIds.find((id) => /session/i.test(id)) ??
|
||||
normalizedIds.find((id) => /work/i.test(id)) ??
|
||||
normalizedIds.find((id) => /draft/i.test(id)) ??
|
||||
normalizedIds.find((id) => /run/i.test(id)) ??
|
||||
normalizedIds.find((id) => /profile/i.test(id)) ??
|
||||
normalizedIds[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function buildDraftCompletionDialogSource(
|
||||
kind: CreationWorkShelfKind,
|
||||
ids: Array<string | null | undefined>,
|
||||
) {
|
||||
const sourceId = pickDraftCompletionDialogSourceId(ids);
|
||||
switch (kind) {
|
||||
case 'rpg':
|
||||
return formatPlatformTaskCompletionSource('RPG 草稿', sourceId);
|
||||
case 'big-fish':
|
||||
return formatPlatformTaskCompletionSource('大鱼吃小鱼草稿', sourceId);
|
||||
case 'match3d':
|
||||
return formatPlatformTaskCompletionSource('抓大鹅草稿', sourceId);
|
||||
case 'square-hole':
|
||||
return formatPlatformTaskCompletionSource('方洞挑战草稿', sourceId);
|
||||
case 'jump-hop':
|
||||
return formatPlatformTaskCompletionSource('跳一跳草稿', sourceId);
|
||||
case 'puzzle':
|
||||
return formatPlatformTaskCompletionSource('拼图草稿', sourceId);
|
||||
case 'visual-novel':
|
||||
return formatPlatformTaskCompletionSource('视觉小说草稿', sourceId);
|
||||
case 'bark-battle':
|
||||
return formatPlatformTaskCompletionSource('汪汪声浪草稿', sourceId);
|
||||
case 'baby-object-match':
|
||||
return formatPlatformTaskCompletionSource('宝贝识物草稿', sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
function createMiniGameDraftGenerationStateForRestoredDraft(
|
||||
kind: MiniGameDraftGenerationKind,
|
||||
metadata?: MiniGameDraftGenerationState['metadata'],
|
||||
@@ -3327,6 +3394,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
useState<DraftGenerationNoticeMap>({});
|
||||
const [pendingDraftShelfItems, setPendingDraftShelfItems] =
|
||||
useState<PendingDraftShelfMap>({});
|
||||
const [
|
||||
pendingPlatformTaskCompletionDialog,
|
||||
setPendingPlatformTaskCompletionDialog,
|
||||
] = useState<
|
||||
| (PlatformTaskCompletionDialogPayload & {
|
||||
key: string;
|
||||
completedAtMs: number | null;
|
||||
})
|
||||
| null
|
||||
>(null);
|
||||
const [initialCreationUrlState] = useState(() => readCreationUrlState());
|
||||
const handledInitialCreationUrlStateRef = useRef(false);
|
||||
const [initialPuzzleRuntimeUrlState] = useState(() =>
|
||||
@@ -3404,10 +3481,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const completedAtMs = status === 'ready' ? Date.now() : undefined;
|
||||
setDraftGenerationNotices((current) => {
|
||||
const next = { ...current };
|
||||
for (const key of uniqueKeys) {
|
||||
next[key] = { status, seen };
|
||||
next[key] =
|
||||
completedAtMs === undefined
|
||||
? { status, seen }
|
||||
: { status, seen, completedAtMs };
|
||||
}
|
||||
return next;
|
||||
});
|
||||
@@ -3449,12 +3530,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
const markDraftGenerating = useCallback(
|
||||
(kind: CreationWorkShelfKind, ids: Array<string | null | undefined>) => {
|
||||
setPendingPlatformTaskCompletionDialog(null);
|
||||
updateDraftGenerationNotices(
|
||||
collectDraftNoticeKeys(kind, ids),
|
||||
'generating',
|
||||
);
|
||||
},
|
||||
[updateDraftGenerationNotices],
|
||||
[setPendingPlatformTaskCompletionDialog, updateDraftGenerationNotices],
|
||||
);
|
||||
const markDraftReady = useCallback(
|
||||
(
|
||||
@@ -3467,17 +3549,27 @@ export function PlatformEntryFlowShellImpl({
|
||||
'ready',
|
||||
viewedImmediately,
|
||||
);
|
||||
if (!viewedImmediately) {
|
||||
const completedAtMs = Date.now();
|
||||
setPendingPlatformTaskCompletionDialog({
|
||||
key: `${kind}:${collectDraftNoticeKeys(kind, ids).join('|')}:${completedAtMs}`,
|
||||
source: buildDraftCompletionDialogSource(kind, ids),
|
||||
message: '生成任务已完成,可以继续查看草稿。',
|
||||
completedAtMs,
|
||||
});
|
||||
}
|
||||
},
|
||||
[updateDraftGenerationNotices],
|
||||
[setPendingPlatformTaskCompletionDialog, updateDraftGenerationNotices],
|
||||
);
|
||||
const markPendingDraftGenerating = useCallback(
|
||||
(
|
||||
kind: Exclude<CreationWorkShelfKind, 'rpg'>,
|
||||
id: string | null | undefined,
|
||||
) => {
|
||||
setPendingPlatformTaskCompletionDialog(null);
|
||||
updatePendingDraftShelfItem(kind, id, 'generating');
|
||||
},
|
||||
[updatePendingDraftShelfItem],
|
||||
[setPendingPlatformTaskCompletionDialog, updatePendingDraftShelfItem],
|
||||
);
|
||||
const markPendingDraftReady = useCallback(
|
||||
(
|
||||
@@ -5790,6 +5882,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
const [dismissedPlatformErrorDialogKey, setDismissedPlatformErrorDialogKey] =
|
||||
useState<string | null>(null);
|
||||
const [
|
||||
dismissedPlatformTaskCompletionDialogKey,
|
||||
setDismissedPlatformTaskCompletionDialogKey,
|
||||
] = useState<string | null>(null);
|
||||
const currentPlatformErrorDialog = useMemo<
|
||||
(PlatformErrorDialogPayload & { key: string }) | null
|
||||
>(() => {
|
||||
@@ -6013,6 +6109,25 @@ export function PlatformEntryFlowShellImpl({
|
||||
woodenFishRun?.runId,
|
||||
woodenFishSession?.sessionId,
|
||||
]);
|
||||
const currentPlatformTaskCompletionDialog = useMemo<
|
||||
| (PlatformTaskCompletionDialogPayload & {
|
||||
key: string;
|
||||
completedAtMs: number | null;
|
||||
})
|
||||
| null
|
||||
>(() => pendingPlatformTaskCompletionDialog, [
|
||||
pendingPlatformTaskCompletionDialog,
|
||||
]);
|
||||
const activePlatformTaskCompletionDialogDismissKey =
|
||||
buildPlatformTaskCompletionDialogDismissKey(
|
||||
currentPlatformTaskCompletionDialog,
|
||||
);
|
||||
const activePlatformTaskCompletionDialog =
|
||||
activePlatformTaskCompletionDialogDismissKey &&
|
||||
activePlatformTaskCompletionDialogDismissKey ===
|
||||
dismissedPlatformTaskCompletionDialogKey
|
||||
? null
|
||||
: currentPlatformTaskCompletionDialog;
|
||||
const activePlatformErrorDialogDismissKey =
|
||||
buildPlatformErrorDialogDismissKey(currentPlatformErrorDialog);
|
||||
const activePlatformErrorDialog =
|
||||
@@ -6118,6 +6233,19 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSquareHoleError,
|
||||
setVisualNovelError,
|
||||
]);
|
||||
const closePlatformTaskCompletionDialog = useCallback(() => {
|
||||
if (!currentPlatformTaskCompletionDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dismissKey = buildPlatformTaskCompletionDialogDismissKey(
|
||||
currentPlatformTaskCompletionDialog,
|
||||
);
|
||||
if (dismissKey) {
|
||||
setDismissedPlatformTaskCompletionDialogKey(dismissKey);
|
||||
}
|
||||
setPendingPlatformTaskCompletionDialog(null);
|
||||
}, [currentPlatformTaskCompletionDialog]);
|
||||
const shouldPollPuzzleGenerationSession =
|
||||
selectionStage === 'puzzle-generating' &&
|
||||
activePuzzleGenerationSessionId != null &&
|
||||
@@ -7116,6 +7244,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsProfilePlayStatsOpen(false);
|
||||
setDraftGenerationNotices({});
|
||||
setPendingDraftShelfItems({});
|
||||
setPendingPlatformTaskCompletionDialog(null);
|
||||
resetRpgSessionViewState();
|
||||
setRpgGeneratedCustomWorldProfile(null);
|
||||
setRpgCustomWorldError(null);
|
||||
@@ -16871,6 +17000,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
|
||||
panelClassName="platform-remap-surface rounded-[1.5rem]"
|
||||
/>
|
||||
<PlatformTaskCompletionDialog
|
||||
completion={activePlatformTaskCompletionDialog}
|
||||
onClose={closePlatformTaskCompletionDialog}
|
||||
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
|
||||
panelClassName="platform-remap-surface rounded-[1.5rem]"
|
||||
/>
|
||||
<UnifiedModal
|
||||
open={Boolean(pendingDeleteCreationWork)}
|
||||
title="删除作品"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import * as clipboardService from '../../services/clipboard';
|
||||
import { PlatformErrorDialog } from './PlatformErrorDialog';
|
||||
import { PlatformTaskCompletionDialog } from './PlatformTaskCompletionDialog';
|
||||
|
||||
vi.mock('../../services/clipboard', () => ({
|
||||
copyTextToClipboard: vi.fn(),
|
||||
@@ -58,3 +59,49 @@ describe('PlatformErrorDialog', () => {
|
||||
expect(screen.queryByRole('dialog', { name: '发生错误' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PlatformTaskCompletionDialog', () => {
|
||||
test('shows source, message, and copies the full completion report', async () => {
|
||||
vi.mocked(clipboardService.copyTextToClipboard).mockResolvedValue(true);
|
||||
|
||||
render(
|
||||
<PlatformTaskCompletionDialog
|
||||
completion={{
|
||||
source: '抓大鹅草稿 match3d-notice-session-1',
|
||||
message: '生成任务已完成,可以继续查看草稿。',
|
||||
}}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '生成完成' });
|
||||
expect(
|
||||
within(dialog).getByText('抓大鹅草稿 match3d-notice-session-1'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByText('生成任务已完成,可以继续查看草稿。'),
|
||||
).toBeTruthy();
|
||||
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '复制内容' }));
|
||||
|
||||
expect(clipboardService.copyTextToClipboard).toHaveBeenCalledWith(
|
||||
[
|
||||
'来源:抓大鹅草稿 match3d-notice-session-1',
|
||||
'状态:生成任务已完成,可以继续查看草稿。',
|
||||
].join('\n'),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: '已复制' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('does not render when there is no active completion', () => {
|
||||
render(
|
||||
<PlatformTaskCompletionDialog completion={null} onClose={() => {}} />,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('dialog', { name: '生成完成' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
124
src/components/platform-entry/PlatformTaskCompletionDialog.tsx
Normal file
124
src/components/platform-entry/PlatformTaskCompletionDialog.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { CheckCircle2, Copy } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { copyTextToClipboard } from '../../services/clipboard';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
|
||||
export type PlatformTaskCompletionDialogPayload = {
|
||||
source: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type PlatformTaskCompletionDialogProps = {
|
||||
completion: PlatformTaskCompletionDialogPayload | null;
|
||||
onClose: () => void;
|
||||
overlayClassName?: string;
|
||||
panelClassName?: string;
|
||||
};
|
||||
|
||||
function buildPlatformTaskCompletionReport(
|
||||
completion: PlatformTaskCompletionDialogPayload,
|
||||
) {
|
||||
return [`来源:${completion.source}`, `状态:${completion.message}`].join(
|
||||
'\n',
|
||||
);
|
||||
}
|
||||
|
||||
export function PlatformTaskCompletionDialog({
|
||||
completion,
|
||||
onClose,
|
||||
overlayClassName = 'platform-theme platform-theme--light !items-center',
|
||||
panelClassName = 'platform-remap-surface rounded-[1.5rem]',
|
||||
}: PlatformTaskCompletionDialogProps) {
|
||||
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>(
|
||||
'idle',
|
||||
);
|
||||
const resetTimerRef = useRef<number | null>(null);
|
||||
const reportText = useMemo(
|
||||
() => (completion ? buildPlatformTaskCompletionReport(completion) : ''),
|
||||
[completion],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (resetTimerRef.current !== null) {
|
||||
window.clearTimeout(resetTimerRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCopyState('idle');
|
||||
}, [completion?.source, completion?.message]);
|
||||
|
||||
const copyCompletion = () => {
|
||||
if (!reportText) {
|
||||
return;
|
||||
}
|
||||
|
||||
void copyTextToClipboard(reportText).then((copied) => {
|
||||
setCopyState(copied ? 'copied' : 'failed');
|
||||
if (resetTimerRef.current !== null) {
|
||||
window.clearTimeout(resetTimerRef.current);
|
||||
}
|
||||
resetTimerRef.current = window.setTimeout(() => {
|
||||
resetTimerRef.current = null;
|
||||
setCopyState('idle');
|
||||
}, 1400);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={Boolean(completion)}
|
||||
title="生成完成"
|
||||
onClose={onClose}
|
||||
size="sm"
|
||||
overlayClassName={overlayClassName}
|
||||
panelClassName={panelClassName}
|
||||
bodyClassName="space-y-3 px-4 py-4 sm:px-5 sm:py-5"
|
||||
footerClassName="justify-end px-4 py-4 sm:px-5"
|
||||
footer={
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyCompletion}
|
||||
disabled={!reportText}
|
||||
className="platform-button platform-button--primary w-full justify-center gap-2 sm:w-auto"
|
||||
>
|
||||
{copyState === 'copied' ? (
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
{copyState === 'copied'
|
||||
? '已复制'
|
||||
: copyState === 'failed'
|
||||
? '复制失败'
|
||||
: '复制内容'}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{completion ? (
|
||||
<>
|
||||
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-2">
|
||||
<div className="text-xs font-bold text-[var(--platform-text-soft)]">
|
||||
来源
|
||||
</div>
|
||||
<div className="mt-1 break-words text-sm font-semibold leading-5 text-[var(--platform-text-strong)]">
|
||||
{completion.source}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-2">
|
||||
<div className="text-xs font-bold text-[var(--platform-text-soft)]">
|
||||
状态
|
||||
</div>
|
||||
<div className="mt-1 whitespace-pre-wrap break-words text-sm leading-6 text-[var(--platform-text-strong)]">
|
||||
{completion.message}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</UnifiedModal>
|
||||
);
|
||||
}
|
||||
@@ -3757,7 +3757,7 @@ test('running match3d form generation can return to draft tab and reopen progres
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(await findCreationTypeButton('抓大鹅'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
@@ -3841,7 +3841,7 @@ test('running match3d persisted draft reopens progress instead of unfinished res
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(await findCreationTypeButton('抓大鹅'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
@@ -4038,7 +4038,7 @@ test('running match3d form generation keeps other creation templates available',
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(await findCreationTypeButton('抓大鹅'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
@@ -4107,7 +4107,7 @@ test('running match3d form generation keeps same template generation available',
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(await findCreationTypeButton('抓大鹅'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
@@ -4721,7 +4721,7 @@ test('match3d draft generation auto starts trial and runtime back opens draft re
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(await findCreationTypeButton('抓大鹅'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
@@ -4953,11 +4953,15 @@ test('completed match3d draft notice first opens trial then reopens result', asy
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(await findCreationTypeButton('抓大鹅'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
expect(await screen.findByText('抓大鹅草稿生成进度')).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('progressbar', {
|
||||
name: '抓大鹅草稿生成进度',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '返回创作中心' }));
|
||||
await openDraftHub(user);
|
||||
await expectDraftHubGeneratingBadgeCountAtLeast(1);
|
||||
@@ -4966,6 +4970,22 @@ test('completed match3d draft notice first opens trial then reopens result', asy
|
||||
resolveCompile({ session: generatedSession });
|
||||
});
|
||||
|
||||
const completionDialog = await screen.findByRole('dialog', {
|
||||
name: '生成完成',
|
||||
});
|
||||
expect(
|
||||
within(completionDialog).getByText(
|
||||
/抓大鹅草稿 match3d-notice-session-1/u,
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(completionDialog).getByText(/生成任务已完成/u),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(completionDialog).getByRole('button', { name: '复制内容' }),
|
||||
).toBeTruthy();
|
||||
await user.click(within(completionDialog).getByLabelText('关闭'));
|
||||
|
||||
expect(await screen.findByLabelText('新生成完成')).toBeTruthy();
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
|
||||
Reference in New Issue
Block a user