Files
makelore/tests/unit/project-publish-action.test.tsx

243 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { act, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ProjectPublishAction } from '@/components/works/ProjectPublishAction';
const publishWorksProjectSourceMock = vi.hoisted(() => vi.fn());
const fetchCurrentWorksProjectStatusMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/works-square', () => ({
publishWorksProjectSource: (...args: unknown[]) => publishWorksProjectSourceMock(...args),
fetchCurrentWorksProjectStatus: (...args: unknown[]) => fetchCurrentWorksProjectStatusMock(...args),
}));
const project = {
id: 'prj_space_cleaner',
path: 'D:/repo/space-cleaner',
name: 'space-cleaner',
createdAt: '',
updatedAt: '',
lastOpenedAt: '',
};
const upload = {
version_id: 'version-1',
review_status: 'building',
build_job_id: 'job-1',
build_status: 'queued',
};
function projectStatus(buildStatus: string, buildErrorCode: string | null = null) {
return {
project: {
app_id: 'space-cleaner',
title: 'space-cleaner',
summary: 'A project',
},
latest_version: {
id: 'version-1',
version_name: 'v1.0.0',
review_status: buildStatus === 'succeeded' ? 'pending_review' : 'build_failed',
change_log: '首次提交',
build_job_id: 'job-1',
build_status: buildStatus,
build_error_code: buildErrorCode,
build_error_message: 'Traceback: /srv/private/builder.py token=secret',
release_id: buildStatus === 'succeeded' ? 'release-1' : null,
created_at: '2026-08-07T12:00:00Z',
},
versions: [],
};
}
async function flushSubmission() {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
}
async function advancePoll() {
await act(async () => {
await vi.advanceTimersByTimeAsync(2_000);
});
}
describe('ProjectPublishAction', () => {
beforeEach(() => {
vi.useFakeTimers();
publishWorksProjectSourceMock.mockReset();
fetchCurrentWorksProjectStatusMock.mockReset();
publishWorksProjectSourceMock.mockResolvedValue({ package: {}, upload });
});
afterEach(() => {
vi.useRealTimers();
});
it('submits automatic metadata and locks after Builder succeeds', async () => {
fetchCurrentWorksProjectStatusMock.mockResolvedValue(projectStatus('succeeded'));
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
expect(screen.getByRole('button', { name: '正在生成本次构建结果…' })).toBeDisabled();
await flushSubmission();
expect(screen.getByTestId('project-publish-status')).toHaveTextContent(
'本次构建结果已通过本地预览检查并上传,正在等待平台校验。',
);
expect(publishWorksProjectSourceMock).toHaveBeenCalledWith({
projectId: 'prj_space_cleaner',
project: {
app_id: 'makelore-prj-space-cleaner',
title: 'space-cleaner',
summary: 'space-cleaner由 Makelore 创建的作品。',
cover_url: null,
category: 'mini_game',
age_band: null,
difficulty: null,
},
});
expect(fetchCurrentWorksProjectStatusMock).not.toHaveBeenCalled();
await advancePoll();
expect(fetchCurrentWorksProjectStatusMock).toHaveBeenCalledWith(
'makelore-prj-space-cleaner',
);
expect(screen.getByRole('button', { name: '已提交,等待运营审核' })).toBeDisabled();
expect(screen.getByTestId('project-publish-status')).toHaveTextContent('已提交,等待运营审核');
});
it('shows a friendly Builder failure without raw details', async () => {
fetchCurrentWorksProjectStatusMock.mockResolvedValue(
projectStatus('failed', 'BROWSER_SMOKE_FAILED'),
);
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
await advancePoll();
const failure = screen.getByTestId('project-publish-failure');
expect(failure).toHaveTextContent('旧版提交无法继续处理');
expect(failure).toHaveTextContent('下一步:升级 Makelore 并重新提交');
expect(failure).not.toHaveTextContent('BROWSER_SMOKE_FAILED');
expect(failure).not.toHaveTextContent('Traceback');
expect(failure).not.toHaveTextContent('/srv/private');
expect(screen.getByRole('button', { name: '重新提交审核' })).toBeEnabled();
});
it('keeps an uncertain submission locked when status polling is interrupted', async () => {
fetchCurrentWorksProjectStatusMock.mockRejectedValue(
new Error('socket closed at /srv/private/status.py'),
);
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
await advancePoll();
const failure = screen.getByTestId('project-publish-failure');
expect(failure).toHaveTextContent('暂时无法读取构建进度');
expect(failure).toHaveTextContent('不需要重复提交');
expect(failure).not.toHaveTextContent('socket closed');
expect(screen.getByRole('button', { name: '已提交,请稍后查看' })).toBeDisabled();
});
it('shows a safe local binding warning while continuing cloud build polling', async () => {
publishWorksProjectSourceMock.mockResolvedValueOnce({
package: {},
upload,
bindingWarning: {
code: 'LOCAL_PREVIEW_BINDING_SAVE_FAILED',
message: '已提交云端,但本机预览绑定保存失败;可重新打开项目/重新提交。',
},
});
fetchCurrentWorksProjectStatusMock.mockResolvedValue(projectStatus('succeeded'));
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
expect(screen.getByTestId('project-publish-binding-warning')).toHaveTextContent(
'已提交云端,但本机预览绑定保存失败;可重新打开项目/重新提交。',
);
expect(screen.getByRole('button', { name: '正在等待云端检查…' })).toBeDisabled();
await advancePoll();
expect(fetchCurrentWorksProjectStatusMock).toHaveBeenCalledOnce();
expect(screen.getByRole('button', { name: '已提交,等待运营审核' })).toBeDisabled();
expect(screen.getByTestId('project-publish-binding-warning')).toBeVisible();
});
it('shows a safe timeout instead of inviting a duplicate submission', async () => {
const status = projectStatus('queued');
status.latest_version.id = 'another-version';
fetchCurrentWorksProjectStatusMock.mockResolvedValue(status);
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
await act(async () => {
await vi.runAllTimersAsync();
});
const failure = screen.getByTestId('project-publish-failure');
expect(failure).toHaveTextContent('构建时间比平时更长');
expect(failure).toHaveTextContent('不需要重复提交');
expect(screen.getByRole('button', { name: '已提交,请稍后查看' })).toBeDisabled();
expect(fetchCurrentWorksProjectStatusMock).toHaveBeenCalledTimes(300);
});
it('sanitizes local and HTTP failures before rendering', async () => {
publishWorksProjectSourceMock.mockRejectedValue(Object.assign(
new Error('Traceback: failed at /srv/private/package.ts token=secret'),
{ code: 'INTERNAL_DATABASE_FAILURE', statusCode: 503 },
));
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
const failure = screen.getByTestId('project-publish-failure');
expect(failure).toHaveTextContent('发布服务暂时不可用');
expect(failure).not.toHaveTextContent('Traceback');
expect(failure).not.toHaveTextContent('INTERNAL_DATABASE_FAILURE');
expect(fetchCurrentWorksProjectStatusMock).not.toHaveBeenCalled();
});
it('turns a local package error into instructions for the user', async () => {
publishWorksProjectSourceMock.mockRejectedValue(Object.assign(
new Error('D:/repo/space-cleaner/package-lock.json does not exist'),
{ code: 'PROJECT_FILE_MISSING' },
));
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
const failure = screen.getByTestId('project-publish-failure');
expect(failure).toHaveTextContent('项目文件不完整');
expect(failure).toHaveTextContent('请让开发助手修复当前项目模板');
expect(failure).not.toHaveTextContent('D:/repo');
expect(failure).not.toHaveTextContent('PROJECT_FILE_MISSING');
expect(fetchCurrentWorksProjectStatusMock).not.toHaveBeenCalled();
});
it('keeps generated metadata within the server limits', async () => {
const longName = `作品-${'x'.repeat(250)}`;
render(<ProjectPublishAction project={{ ...project, name: longName }} projectType="mini_program" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
const input = publishWorksProjectSourceMock.mock.calls[0]?.[0];
expect(input.project.app_id).toBe('makelore-prj-space-cleaner');
expect(input.project.title).toHaveLength(160);
expect(input.project.summary).toBe(`${input.project.title},由 Makelore 创建的作品。`);
expect(input.project.category).toBe('mini_program');
});
});