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

239 lines
9.5 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(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('下一步:请让开发助手运行 npm run build');
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');
});
});