需求:让非专业用户在项目操作区一次提交,运营审核通过后直接发布。 实现:由 Electron Main 完成安全打包、自动版本、幂等重试和状态脱敏;补齐友好失败反馈、唯一提交入口及隔离 Electron E2E fixture。
211 lines
8.1 KiB
TypeScript
211 lines
8.1 KiB
TypeScript
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 创建的作品。`);
|
||
});
|
||
});
|