Files
makelore/tests/unit/project-publish-action.test.tsx
brother7 724290e864 实现 Makelore 一键提交审核
需求:让非专业用户在项目操作区一次提交,运营审核通过后直接发布。

实现:由 Electron Main 完成安全打包、自动版本、幂等重试和状态脱敏;补齐友好失败反馈、唯一提交入口及隔离 Electron E2E fixture。
2026-08-08 14:44:08 +08:00

211 lines
8.1 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} />);
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: 'web',
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} />);
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} />);
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 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} />);
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} />);
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} />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
const failure = screen.getByTestId('project-publish-failure');
expect(failure).toHaveTextContent('项目文件不完整');
expect(failure).toHaveTextContent('请让开发助手补齐 package.json、package-lock.json 和 index.html');
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 }} />);
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 创建的作品。`);
});
});