实现网站游戏评分与评价 (#561)
Project CI / AI game creator shell Rust crates (push) Successful in 1m43s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 2m7s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m55s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 3m23s
Project CI / Frontend tests (push) Successful in 3m3s
Project CI / Repository checks (push) Successful in 3m42s
Project CI / AI game creator shell web tests (push) Successful in 2m3s
Project CI / Backend tests (push) Successful in 6m19s
Project CI / Native shell tests (push) Successful in 8m7s
Project CI / AI game creator shell Rust crates (push) Successful in 1m43s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 2m7s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m55s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 3m23s
Project CI / Frontend tests (push) Successful in 3m3s
Project CI / Repository checks (push) Successful in 3m42s
Project CI / AI game creator shell web tests (push) Successful in 2m3s
Project CI / Backend tests (push) Successful in 6m19s
Project CI / Native shell tests (push) Successful in 8m7s
公开游戏详情页新增评分与评价:登录用户可提交并修改自己对每款游戏的唯一一条 1–10 分评价,评论可空且最多 4000 个 Unicode 码点;游客可分页浏览评价并查看平均分与评分人数。 - 新增私有评价表及共享 DTO/API,事务内校验游戏公开可见性、保证唯一性,并计算分页与全量评分统计;下架或封禁后关闭读写,重新公开及更新版本保留评价。 - 详情页支持编辑预填、取消、提交期间冻结输入、失败保留草稿,以及保存、翻页和账号/游戏切换时的旧响应隔离;复用现有认证与公共组件。 - 同步主规范、里程碑、实施计划、生成绑定、表目录和验收脚本。不扩展 AGC、外部 API、评论审核或评分缓存。 验证: - 本轮审查复跑前端定向测试 40 项、评价领域测试 2 项,均通过。 - 网站 typecheck、Rust/TypeScript DTO 对齐、SpacetimeDB schema、文档索引、编码与 diff 检查通过。 - 实施文档另记录本地隔离数据库/API 49 项及桌面/移动视口浏览器 smoke 通过;本轮审查未重跑这些运行时验收,也未运行完整测试套件。 发布说明:新增 SpacetimeDB 表,部署时需同步 module 与 API。尚未部署 dev/production,待用户验收。 Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/561 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #561.
This commit is contained in:
@@ -117,6 +117,20 @@ beforeEach(() => {
|
||||
});
|
||||
});
|
||||
|
||||
test('有评价权限时游戏管理提供带gameId的评价入口', async () => {
|
||||
window.history.replaceState(null, '', '#game-management');
|
||||
render(
|
||||
<AdminGameManagementPage
|
||||
token="token"
|
||||
canManageReviews
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '查看评价' }));
|
||||
expect(window.location.hash).toBe('#game-reviews?gameId=game_1');
|
||||
window.history.replaceState(null, '', '/');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
@@ -28,6 +28,7 @@ import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminGameManagementPageProps {
|
||||
token: string;
|
||||
canManageReviews?: boolean;
|
||||
onUnauthorized: (message?: string) => void;
|
||||
}
|
||||
|
||||
@@ -86,6 +87,7 @@ function createGameActionIdempotencyKey(
|
||||
|
||||
export function AdminGameManagementPage({
|
||||
token,
|
||||
canManageReviews = false,
|
||||
onUnauthorized,
|
||||
}: AdminGameManagementPageProps) {
|
||||
const [games, setGames] = useState<AdminGameDistributionGameEntry[]>([]);
|
||||
@@ -327,6 +329,17 @@ export function AdminGameManagementPage({
|
||||
>
|
||||
版本历史
|
||||
</AdminButton>
|
||||
{canManageReviews ? (
|
||||
<AdminButton
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.location.hash = `#game-reviews?gameId=${encodeURIComponent(entry.gameId)}`;
|
||||
}}
|
||||
>
|
||||
查看评价
|
||||
</AdminButton>
|
||||
) : null}
|
||||
</AdminActionRow>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getAdminGameReview,
|
||||
listAdminGameReviewGames,
|
||||
listAdminGameReviews,
|
||||
moderateAdminGameReview,
|
||||
} from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminGameReview,
|
||||
AdminGameReviewDetailResponse,
|
||||
AdminGameReviewsResponse,
|
||||
} from '../api/adminApiTypes';
|
||||
import { AdminGameReviewsPage } from './AdminGameReviewsPage';
|
||||
|
||||
vi.mock('../api/adminApiClient', () => ({
|
||||
listAdminGameReviews: vi.fn(),
|
||||
listAdminGameReviewGames: vi.fn(),
|
||||
getAdminGameReview: vi.fn(),
|
||||
moderateAdminGameReview: vi.fn(),
|
||||
isAdminApiError: (error: unknown) =>
|
||||
typeof error === 'object' && error !== null && 'status' in error,
|
||||
formatAdminApiError: (error: unknown) =>
|
||||
error instanceof Error ? error.message : '请求失败',
|
||||
}));
|
||||
|
||||
const review: AdminGameReview = {
|
||||
id: '6:game_1user_1',
|
||||
gameId: 'game_1',
|
||||
game: { title: '测试游戏', status: 'published' },
|
||||
author: { id: 'user_1', name: '用户甲' },
|
||||
score: 8,
|
||||
comment: '<b>中文😀</b>\n完整正文',
|
||||
isHidden: false,
|
||||
createdAt: '2026-10-01T01:00:00Z',
|
||||
updatedAt: '2026-10-01T01:00:00Z',
|
||||
};
|
||||
function list(
|
||||
overrides: Partial<AdminGameReviewsResponse> = {},
|
||||
): AdminGameReviewsResponse {
|
||||
return {
|
||||
reviews: [review],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
const unauthorized = vi.fn();
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
window.history.replaceState(null, '', '#game-reviews');
|
||||
vi.mocked(listAdminGameReviews).mockResolvedValue(list());
|
||||
vi.mocked(listAdminGameReviewGames).mockResolvedValue({
|
||||
games: [{ gameId: 'game_2', title: '第二个游戏', status: 'suspended' }],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
});
|
||||
vi.mocked(getAdminGameReview).mockResolvedValue({ review, operations: [] });
|
||||
vi.mocked(moderateAdminGameReview).mockResolvedValue({
|
||||
review: { ...review, isHidden: true },
|
||||
operation: {
|
||||
id: 'op',
|
||||
reviewId: review.id,
|
||||
gameId: review.gameId,
|
||||
userId: review.author.id,
|
||||
reviewCreatedAt: review.createdAt,
|
||||
action: 'hide',
|
||||
adminUserId: 'admin',
|
||||
reason: '广告',
|
||||
createdAt: review.createdAt,
|
||||
},
|
||||
replayed: false,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
window.history.replaceState(null, '', '/');
|
||||
});
|
||||
function show() {
|
||||
return render(
|
||||
<AdminGameReviewsPage token="token" onUnauthorized={unauthorized} />,
|
||||
);
|
||||
}
|
||||
async function row() {
|
||||
return (await screen.findByText('用户甲')).closest('tr')!;
|
||||
}
|
||||
|
||||
test('入口游戏参数和hash变化生效,组合筛选分页在服务端查询,重置回第一页', async () => {
|
||||
window.history.replaceState(null, '', '#game-reviews?gameId=game_1');
|
||||
vi.mocked(listAdminGameReviews).mockResolvedValue(
|
||||
list({ total: 21, totalPages: 2 }),
|
||||
);
|
||||
show();
|
||||
await row();
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({ gameId: 'game_1', page: 1 }),
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('评价用户 ID'), {
|
||||
target: { value: 'user_1' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('评论关键词'), {
|
||||
target: { value: '中文' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('评价状态'), {
|
||||
target: { value: 'hidden' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '查询' }));
|
||||
await waitFor(() =>
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith('token', {
|
||||
gameId: 'game_1',
|
||||
userId: 'user_1',
|
||||
keyword: '中文',
|
||||
status: 'hidden',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
}),
|
||||
);
|
||||
await row();
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('navigation', { name: '评价分页' })).getByRole(
|
||||
'button',
|
||||
{ name: '下一页' },
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({ page: 2, keyword: '中文' }),
|
||||
),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '重置' }));
|
||||
await waitFor(() =>
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith('token', {
|
||||
gameId: '',
|
||||
userId: '',
|
||||
keyword: '',
|
||||
status: 'all',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
}),
|
||||
);
|
||||
act(() => {
|
||||
window.history.replaceState(null, '', '#game-reviews?gameId=game_2');
|
||||
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({ gameId: 'game_2', page: 1 }),
|
||||
),
|
||||
);
|
||||
expect((screen.getByLabelText('游戏 ID') as HTMLInputElement).value).toBe(
|
||||
'game_2',
|
||||
);
|
||||
});
|
||||
|
||||
test('名称查找包含下架游戏,选择后按游戏ID筛选', async () => {
|
||||
show();
|
||||
await row();
|
||||
fireEvent.click(screen.getByRole('button', { name: '按名称查找游戏' }));
|
||||
const dialog = screen.getByRole('dialog', { name: '查找游戏' });
|
||||
await within(dialog).findByText('第二个游戏');
|
||||
fireEvent.change(within(dialog).getByLabelText('游戏名称或 ID'), {
|
||||
target: { value: '第二个' },
|
||||
});
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '搜索' }));
|
||||
await waitFor(() =>
|
||||
expect(listAdminGameReviewGames).toHaveBeenLastCalledWith('token', {
|
||||
query: '第二个',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
}),
|
||||
);
|
||||
await within(dialog).findByText('第二个游戏');
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '选择' }));
|
||||
expect((screen.getByLabelText('游戏 ID') as HTMLInputElement).value).toBe(
|
||||
'game_2',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '查询' }));
|
||||
await waitFor(() =>
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({ gameId: 'game_2' }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('隐藏原因必填,失败保留草稿,同次重试复用幂等key,取消不写入', async () => {
|
||||
vi.mocked(moderateAdminGameReview).mockRejectedValueOnce(
|
||||
new Error('网络失败'),
|
||||
);
|
||||
show();
|
||||
fireEvent.click(within(await row()).getByRole('button', { name: '隐藏' }));
|
||||
let dialog = screen.getByRole('dialog', { name: '隐藏评价' });
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '确认隐藏' }));
|
||||
expect(
|
||||
await within(dialog).findByText('原因必须为 1–4000 个字符'),
|
||||
).toBeTruthy();
|
||||
expect(moderateAdminGameReview).not.toHaveBeenCalled();
|
||||
fireEvent.change(within(dialog).getByLabelText('操作原因(必填)'), {
|
||||
target: { value: ' 广告😀 ' },
|
||||
});
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '确认隐藏' }));
|
||||
await within(dialog).findByText('网络失败');
|
||||
expect(
|
||||
(within(dialog).getByLabelText('操作原因(必填)') as HTMLTextAreaElement)
|
||||
.value,
|
||||
).toBe(' 广告😀 ');
|
||||
const first = vi.mocked(moderateAdminGameReview).mock.calls[0]!;
|
||||
expect(first[3]).toEqual({
|
||||
action: 'hide',
|
||||
expectedCreatedAt: review.createdAt,
|
||||
reason: '广告😀',
|
||||
});
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '确认隐藏' }));
|
||||
await screen.findByText('评价已隐藏');
|
||||
expect(vi.mocked(moderateAdminGameReview).mock.calls[1]![2]).toBe(first[2]);
|
||||
fireEvent.click(within(await row()).getByRole('button', { name: '删除' }));
|
||||
dialog = screen.getByRole('dialog', { name: '删除评价' });
|
||||
expect(
|
||||
within(dialog).getByText('删除后不可恢复,用户可以重新评价'),
|
||||
).toBeTruthy();
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '取消' }));
|
||||
expect(moderateAdminGameReview).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('4001码点原因不能提交,恢复复用确认且不要求原因', async () => {
|
||||
vi.mocked(listAdminGameReviews).mockResolvedValue(
|
||||
list({ reviews: [{ ...review, isHidden: true }] }),
|
||||
);
|
||||
show();
|
||||
fireEvent.click(within(await row()).getByRole('button', { name: '删除' }));
|
||||
const deletion = screen.getByRole('dialog', { name: '删除评价' });
|
||||
fireEvent.change(within(deletion).getByLabelText('操作原因(必填)'), {
|
||||
target: { value: '😀'.repeat(4001) },
|
||||
});
|
||||
expect(within(deletion).getByText('4001/4000')).toBeTruthy();
|
||||
expect(
|
||||
(
|
||||
within(deletion).getByRole('button', {
|
||||
name: '确认删除',
|
||||
}) as HTMLButtonElement
|
||||
).disabled,
|
||||
).toBe(true);
|
||||
fireEvent.click(within(deletion).getByRole('button', { name: '取消' }));
|
||||
fireEvent.click(within(await row()).getByRole('button', { name: '恢复' }));
|
||||
const confirmation = screen.getByRole('dialog', { name: '确认操作' });
|
||||
fireEvent.click(within(confirmation).getByRole('button', { name: '确认' }));
|
||||
await waitFor(() =>
|
||||
expect(moderateAdminGameReview).toHaveBeenCalledWith(
|
||||
'token',
|
||||
review.id,
|
||||
expect.any(String),
|
||||
{ action: 'restore', expectedCreatedAt: review.createdAt },
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('删除使当前页越界时回到最后一页并保留条件', async () => {
|
||||
vi.mocked(listAdminGameReviews).mockImplementation(async (_token, query) =>
|
||||
list({ page: query?.page ?? 1, total: 21, totalPages: 2 }),
|
||||
);
|
||||
show();
|
||||
await row();
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('navigation', { name: '评价分页' })).getByRole(
|
||||
'button',
|
||||
{ name: '下一页' },
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({ page: 2 }),
|
||||
),
|
||||
);
|
||||
fireEvent.click(within(await row()).getByRole('button', { name: '删除' }));
|
||||
const dialog = screen.getByRole('dialog', { name: '删除评价' });
|
||||
fireEvent.change(within(dialog).getByLabelText('操作原因(必填)'), {
|
||||
target: { value: '重复内容' },
|
||||
});
|
||||
vi.mocked(listAdminGameReviews).mockResolvedValue(
|
||||
list({ total: 20, totalPages: 1 }),
|
||||
);
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '确认删除' }));
|
||||
await waitFor(() =>
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({ page: 1 }),
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('评价已删除')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('详情完整纯文本只读并展示后台操作原因', async () => {
|
||||
vi.mocked(getAdminGameReview).mockResolvedValue({
|
||||
review,
|
||||
operations: [
|
||||
{
|
||||
id: 'op',
|
||||
reviewId: review.id,
|
||||
gameId: review.gameId,
|
||||
userId: review.author.id,
|
||||
reviewCreatedAt: review.createdAt,
|
||||
action: 'hide',
|
||||
adminUserId: 'admin_1',
|
||||
reason: '广告内容',
|
||||
createdAt: review.createdAt,
|
||||
},
|
||||
],
|
||||
});
|
||||
show();
|
||||
fireEvent.click(
|
||||
within(await row()).getByRole('button', { name: '查看详情' }),
|
||||
);
|
||||
const dialog = screen.getByRole('dialog', { name: '评价详情' });
|
||||
expect(await within(dialog).findByText('广告内容')).toBeTruthy();
|
||||
expect(dialog.textContent).toContain(review.comment);
|
||||
expect(dialog.querySelector('b')).toBeNull();
|
||||
expect(within(dialog).getByText('admin_1')).toBeTruthy();
|
||||
expect(within(dialog).queryByRole('textbox')).toBeNull();
|
||||
});
|
||||
|
||||
test('详情关闭后旧响应不会覆盖新目标', async () => {
|
||||
const second = {
|
||||
...review,
|
||||
id: 'review_2',
|
||||
author: { id: 'user_2', name: '用户乙' },
|
||||
comment: '第二条正文',
|
||||
};
|
||||
vi.mocked(listAdminGameReviews).mockResolvedValue(
|
||||
list({ reviews: [review, second], total: 2 }),
|
||||
);
|
||||
let resolveOld!: (value: { review: AdminGameReview; operations: [] }) => void;
|
||||
vi.mocked(getAdminGameReview)
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<AdminGameReviewDetailResponse>((resolve) => {
|
||||
resolveOld = resolve;
|
||||
}),
|
||||
)
|
||||
.mockResolvedValue({ review: second, operations: [] });
|
||||
show();
|
||||
fireEvent.click(
|
||||
within(await row()).getByRole('button', { name: '查看详情' }),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog')).getByRole('button', { name: '关闭' }),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByText('用户乙').closest('tr')!).getByRole('button', {
|
||||
name: '查看详情',
|
||||
}),
|
||||
);
|
||||
await within(screen.getByRole('dialog')).findByText('第二条正文');
|
||||
await act(async () => {
|
||||
resolveOld({ review, operations: [] });
|
||||
});
|
||||
expect(
|
||||
within(screen.getByRole('dialog')).getByText('第二条正文'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('dialog').textContent).not.toContain(review.comment);
|
||||
});
|
||||
|
||||
test('管理提交中切换游戏,成功后刷新当前游戏而不恢复旧筛选', async () => {
|
||||
let finish!: (
|
||||
value: Awaited<ReturnType<typeof moderateAdminGameReview>>,
|
||||
) => void;
|
||||
vi.mocked(moderateAdminGameReview).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<Awaited<ReturnType<typeof moderateAdminGameReview>>>(
|
||||
(resolve) => {
|
||||
finish = resolve;
|
||||
},
|
||||
),
|
||||
);
|
||||
window.history.replaceState(null, '', '#game-reviews?gameId=game_1');
|
||||
show();
|
||||
fireEvent.click(within(await row()).getByRole('button', { name: '隐藏' }));
|
||||
const dialog = screen.getByRole('dialog', { name: '隐藏评价' });
|
||||
fireEvent.change(within(dialog).getByLabelText('操作原因(必填)'), {
|
||||
target: { value: '广告' },
|
||||
});
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '确认隐藏' }));
|
||||
act(() => {
|
||||
window.history.replaceState(null, '', '#game-reviews?gameId=game_2');
|
||||
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({ gameId: 'game_2' }),
|
||||
),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(await row()).getByRole('button', { name: '查看详情' }),
|
||||
);
|
||||
await within(screen.getByRole('dialog', { name: '评价详情' })).findByText(
|
||||
'评价 ID',
|
||||
);
|
||||
await act(async () => {
|
||||
finish({
|
||||
review: null,
|
||||
operation: {
|
||||
id: 'op',
|
||||
reviewId: review.id,
|
||||
gameId: review.gameId,
|
||||
userId: review.author.id,
|
||||
reviewCreatedAt: review.createdAt,
|
||||
action: 'hide',
|
||||
adminUserId: 'admin',
|
||||
reason: '广告',
|
||||
createdAt: review.createdAt,
|
||||
},
|
||||
replayed: false,
|
||||
});
|
||||
});
|
||||
expect(listAdminGameReviews).toHaveBeenLastCalledWith(
|
||||
'token',
|
||||
expect.objectContaining({ gameId: 'game_2' }),
|
||||
);
|
||||
expect((screen.getByLabelText('游戏 ID') as HTMLInputElement).value).toBe(
|
||||
'game_2',
|
||||
);
|
||||
expect(screen.getByRole('dialog', { name: '评价详情' })).toBeTruthy();
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user