实现小游戏与小程序项目创建发布

This commit is contained in:
2026-08-09 13:31:06 +08:00
parent a6fe14a8d6
commit 493b31cff0
26 changed files with 1827 additions and 39 deletions

View File

@@ -16,6 +16,12 @@ test.describe('Project-level Superpowers setting', () => {
const page = await getStableWindow(app);
await page.getByTestId('sidebar-create-project').click();
await expect(page.getByRole('radio', { name: '小游戏' })).toBeChecked();
await expect(page.getByRole('radio', { name: '小程序' })).not.toBeChecked();
await expect(page.getByRole('radio', { name: '自定义项目' })).not.toBeChecked();
await page.getByRole('radio', { name: '小程序' }).click();
await expect(page.getByRole('radio', { name: '小程序' })).toBeChecked();
await page.getByRole('radio', { name: '小游戏' }).click();
await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked();
await page.getByRole('button', { name: '选择路径' }).click();
await expect(page.getByLabel('项目路径')).toHaveValue(parentPath);

View File

@@ -515,6 +515,53 @@ describe('opencode host api routes', () => {
}
});
it.each([
['mini_game', ['game.json', 'game.js']],
['mini_program', ['app.json', 'pages/index/index.js']],
] as const)('creates a complete %s project template', async (projectType, expectedTypeFiles) => {
const parentPath = await mkdtemp(join(tmpdir(), `niancode-create-${projectType}-`));
try {
const response = createResponse();
const expectedProjectPath = join(parentPath, '新作品');
const project = {
id: `prj_${projectType}`,
path: expectedProjectPath,
name: '新作品',
};
const rememberProject = vi.fn(async () => project);
const handled = await handleOpencodeRoutes(
createRequest('POST', { parentPath, projectName: '新作品', projectType }),
response.res,
new URL('http://127.0.0.1/api/opencode/projects/create'),
{
opencodeProjectStore: {
rememberProject,
listProjects: vi.fn(async () => [project]),
getActiveProject: vi.fn(async () => null),
},
} as never,
);
expect(handled).toBe(true);
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({
success: true,
config: { projectType },
});
const config = JSON.parse(await readFile(join(expectedProjectPath, '.niancode', 'project.json'), 'utf8')) as Record<string, unknown>;
expect(config.projectType).toBe(projectType);
await expect(stat(join(expectedProjectPath, 'package.json'))).resolves.toBeTruthy();
await expect(stat(join(expectedProjectPath, 'package-lock.json'))).resolves.toBeTruthy();
await expect(stat(join(expectedProjectPath, 'index.html'))).resolves.toBeTruthy();
for (const relativePath of expectedTypeFiles) {
await expect(stat(join(expectedProjectPath, relativePath))).resolves.toBeTruthy();
}
} finally {
await rm(parentPath, { recursive: true, force: true });
}
});
it('initializes the selected non-empty folder in place without changing existing files', async () => {
const projectPath = await mkdtemp(join(tmpdir(), 'niancode-direct-project-'));
try {

View File

@@ -3650,6 +3650,7 @@ describe('opencode store', () => {
directoryMode: 'create-child-directory',
selectedPath: '/Users/kid/NianCode',
projectName: '星星收集器',
projectType: 'mini_game',
});
expect(opened).toEqual(project);
@@ -3658,6 +3659,7 @@ describe('opencode store', () => {
body: JSON.stringify({
parentPath: '/Users/kid/NianCode',
projectName: '星星收集器',
projectType: 'mini_game',
}),
});
expect(useOpencodeStore.getState().activeProject).toBeNull();
@@ -3676,6 +3678,7 @@ describe('opencode store', () => {
const opened = await useOpencodeStore.getState().createProject({
directoryMode: 'use-selected-directory',
selectedPath: '/Users/kid/NianCode/星星收集器',
projectType: 'custom',
});
expect(opened).toEqual(project);
@@ -3683,6 +3686,7 @@ describe('opencode store', () => {
method: 'POST',
body: JSON.stringify({
projectPath: '/Users/kid/NianCode/星星收集器',
projectType: 'custom',
}),
});
expect(useOpencodeStore.getState().activeProject).toBeNull();

View File

@@ -45,6 +45,7 @@ describe('project-owned contact configuration', () => {
it('creates an empty, template-free project config', () => {
const config = createProjectConfig('2026-07-11T00:00:00.000Z');
expect(config).toMatchObject({
projectType: 'custom',
initialized: false,
superpowersEnabled: false,
agents: [],
@@ -53,6 +54,19 @@ describe('project-owned contact configuration', () => {
expect('templateId' in config).toBe(false);
});
it('normalizes legacy configs without a project type as custom', () => {
const legacyConfig = { ...createProjectConfig(), projectType: undefined };
expect(normalizeProjectConfig(legacyConfig).projectType).toBe('custom');
expect(normalizeProjectConfig({
...legacyConfig,
projectType: 'mini_game',
}).projectType).toBe('mini_game');
expect(() => normalizeProjectConfig({
...legacyConfig,
projectType: 'future_project',
})).toThrow('Invalid project type');
});
it('requires a unique name, preset avatar, model, and responsibility for every contact', () => {
expect(validateAgentConfigs([])).toEqual([]);
expect(validateAgentConfigs([createContact({ name: '' })])).toContain('agent-test:name-required');
@@ -130,4 +144,18 @@ describe('project-owned contact configuration', () => {
agents: [createContact({ model: null })],
})).rejects.toThrow('model-required');
});
it('does not allow the persisted project type to change', async () => {
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-type-'));
const initial = await createInitialProjectConfig(projectPath, { projectType: 'mini_game' });
await expect(writeProjectConfig(projectPath, {
...initial,
projectType: 'mini_program',
})).rejects.toThrow('已有项目类型不可更改');
await expect(readProjectConfig(projectPath)).resolves.toMatchObject({
status: 'valid',
config: { projectType: 'mini_game' },
});
});
});

View File

@@ -52,7 +52,7 @@ describe('ProjectConfiguration', () => {
dispatchEvent: vi.fn(),
})),
});
const config = createProjectConfig();
const config = createProjectConfig(undefined, 'mini_game');
useOpencodeStore.setState({ activeProject: project, projects: [project] });
useProjectConfigStore.setState({ configsByProjectId: { [project.id]: config }, knowledgeByProjectId: { [project.id]: [] }, errorsByProjectId: {}, loadingProjectId: null });
useProviderStore.setState({
@@ -85,6 +85,29 @@ describe('ProjectConfiguration', () => {
expect(screen.queryAllByTestId(/^project-agent-/)).toHaveLength(0);
});
it('does not offer one-click submission for custom projects', async () => {
const customConfig = createProjectConfig();
useProjectConfigStore.setState({
configsByProjectId: { [project.id]: customConfig },
knowledgeByProjectId: { [project.id]: [] },
errorsByProjectId: {},
loadingProjectId: null,
});
const fallback = hostApiFetchMock.getMockImplementation();
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
if (path === `/api/opencode/projects/config?projectId=${project.id}`) {
return { status: 'valid', config: customConfig, knowledgeFiles: [] };
}
if (!fallback) throw new Error(`Unexpected path ${path}`);
return fallback(path, init);
});
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
expect(await screen.findByText('自定义项目暂未配置发布方式。如需一键提交,请新建小游戏或小程序项目。')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: '一键提交审核' })).not.toBeInTheDocument();
});
it('creates a contact with the required basics and leaves advanced settings empty', async () => {
render(<MemoryRouter><ProjectConfiguration /></MemoryRouter>);
fireEvent.click(await screen.findByRole('button', { name: '新增伙伴' }));

View File

@@ -3,6 +3,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { initializeProjectDirectory } from '@electron/opencode/project-directory-initialization';
import type { ProjectType } from '../../shared/project-config';
const temporaryDirectories: string[] = [];
@@ -13,6 +14,98 @@ afterEach(async () => {
});
describe('project directory initialization', () => {
it.each([
['mini_game', ['game.json', 'game.js']],
['mini_program', ['app.json', 'app.js', join('pages', 'index', 'index.js')]],
] satisfies Array<[ProjectType, string[]]>)('creates a complete %s Vite project template', async (projectType, typeFiles) => {
const parentPath = await mkdtemp(join(tmpdir(), 'makelore-project-parent-'));
temporaryDirectories.push(parentPath);
const projectPath = join(parentPath, projectType);
const result = await initializeProjectDirectory({
projectPath,
projectType,
allowExistingDirectory: false,
});
expect(result).toMatchObject({
reusedExistingConfig: false,
config: { projectType },
});
const packageJson = JSON.parse(await readFile(join(projectPath, 'package.json'), 'utf8')) as {
scripts: Record<string, string>;
devDependencies: Record<string, string>;
};
const packageLock = JSON.parse(await readFile(join(projectPath, 'package-lock.json'), 'utf8')) as {
lockfileVersion: number;
packages: Record<string, unknown>;
};
expect(packageJson.scripts.build).toBe('vite build');
expect(packageJson.devDependencies.vite).toBe('7.3.1');
expect(packageLock.lockfileVersion).toBe(3);
expect(packageLock.packages['node_modules/vite']).toBeTruthy();
await expect(readFile(join(projectPath, 'index.html'), 'utf8')).resolves.toContain('type="module"');
for (const relativePath of typeFiles) {
await expect(readFile(join(projectPath, relativePath), 'utf8')).resolves.not.toBe('');
}
});
it('keeps custom projects on the existing minimal project structure', async () => {
const parentPath = await mkdtemp(join(tmpdir(), 'makelore-custom-parent-'));
temporaryDirectories.push(parentPath);
const projectPath = join(parentPath, 'custom-project');
const result = await initializeProjectDirectory({
projectPath,
projectType: 'custom',
allowExistingDirectory: false,
});
expect(result.config.projectType).toBe('custom');
expect((await readdir(projectPath)).sort()).toEqual(['.niancode', 'TASKS.md', 'VERSION.md', 'knowledge']);
});
it('reuses an existing project only when an explicitly selected type matches', async () => {
const parentPath = await mkdtemp(join(tmpdir(), 'makelore-existing-parent-'));
temporaryDirectories.push(parentPath);
const projectPath = join(parentPath, 'existing-project');
await initializeProjectDirectory({
projectPath,
projectType: 'mini_game',
allowExistingDirectory: false,
});
await expect(initializeProjectDirectory({
projectPath,
projectType: 'mini_game',
allowExistingDirectory: true,
})).resolves.toMatchObject({ reusedExistingConfig: true, config: { projectType: 'mini_game' } });
await expect(initializeProjectDirectory({
projectPath,
projectType: 'mini_program',
allowExistingDirectory: true,
})).rejects.toThrow('已有项目类型不可更改');
await expect(initializeProjectDirectory({
projectPath,
allowExistingDirectory: true,
})).resolves.toMatchObject({ reusedExistingConfig: true, config: { projectType: 'mini_game' } });
});
it('does not change an existing folder when a publishable template file conflicts', async () => {
const projectPath = await mkdtemp(join(tmpdir(), 'makelore-template-conflict-'));
temporaryDirectories.push(projectPath);
await writeFile(join(projectPath, 'package.json'), '{"name":"user-project"}\n', 'utf8');
await expect(initializeProjectDirectory({
projectPath,
projectType: 'mini_game',
allowExistingDirectory: true,
})).rejects.toThrow('Project initialization conflict: package.json already exists');
expect((await readdir(projectPath)).sort()).toEqual(['package.json']);
await expect(readFile(join(projectPath, 'package.json'), 'utf8')).resolves.toBe('{"name":"user-project"}\n');
});
it('rolls back only files and directories created by a failed in-place merge', async () => {
const projectPath = await mkdtemp(join(tmpdir(), 'niancode-in-place-rollback-'));
temporaryDirectories.push(projectPath);

View File

@@ -18,6 +18,7 @@ import {
createStaticProjectPackage,
ProjectPackageError,
} from '@electron/services/project-packager';
import { createProjectConfig, type ProjectType } from '../../shared/project-config';
const openRaceHook = vi.hoisted(() => ({
beforeOpen: null as null | ((path: string) => Promise<void>),
@@ -39,11 +40,17 @@ const AdmZip = require('adm-zip') as typeof import('adm-zip');
const tempDirectories: string[] = [];
async function createProject(): Promise<string> {
async function createProject(projectType: ProjectType = 'mini_game'): Promise<string> {
const projectPath = await mkdtemp(join(tmpdir(), 'makelore-project-packager-'));
tempDirectories.push(projectPath);
await mkdir(join(projectPath, 'src'), { recursive: true });
await mkdir(join(projectPath, 'public'), { recursive: true });
await mkdir(join(projectPath, '.niancode'), { recursive: true });
await writeFile(
join(projectPath, '.niancode', 'project.json'),
JSON.stringify(createProjectConfig('2026-08-09T00:00:00.000Z', projectType)),
'utf8',
);
await writeFile(
join(projectPath, 'package.json'),
JSON.stringify({
@@ -83,7 +90,6 @@ describe('project packager', () => {
await mkdir(join(projectPath, 'node_modules', 'vite'), { recursive: true });
await mkdir(join(projectPath, '.git'), { recursive: true });
await mkdir(join(projectPath, 'dist'), { recursive: true });
await mkdir(join(projectPath, '.niancode'), { recursive: true });
for (const directory of ['.ssh', '.aws', '.gnupg', '.kube', '.docker', 'secrets']) {
await mkdir(join(projectPath, directory), { recursive: true });
await writeFile(join(projectPath, directory, 'credential'), 'private', 'utf8');
@@ -91,7 +97,7 @@ describe('project packager', () => {
await writeFile(join(projectPath, 'node_modules', 'vite', 'index.js'), 'generated', 'utf8');
await writeFile(join(projectPath, '.git', 'config'), 'private', 'utf8');
await writeFile(join(projectPath, 'dist', 'index.html'), 'built', 'utf8');
await writeFile(join(projectPath, '.niancode', 'project.json'), '{}', 'utf8');
await writeFile(join(projectPath, '.niancode', 'local-state.json'), '{}', 'utf8');
await writeFile(join(projectPath, '.env'), 'TOKEN=secret', 'utf8');
await writeFile(join(projectPath, '.env.example'), 'TOKEN=replace-me', 'utf8');
await writeFile(join(projectPath, '.npmrc'), '//registry/:_authToken=secret', 'utf8');
@@ -170,6 +176,7 @@ describe('project packager', () => {
]);
expect(archive.readAsText('niancode.yml')).toBe(
'schema_version: 1\n'
+ 'project_type: mini_game\n'
+ 'kind: web\n'
+ 'runtime: static\n'
+ 'build:\n'
@@ -177,10 +184,44 @@ describe('project packager', () => {
+ ' package_manager: npm\n'
+ ' entry: index.html\n',
);
expect(first.manifest.project_type).toBe('mini_game');
expect(entries.every((entry) => entry.header.time.getFullYear() === 1980)).toBe(true);
await expect(readFile(join(projectPath, 'niancode.yml'), 'utf8')).resolves.toBe('runtime: compose\n');
});
it('writes the selected publishable project type into the generated manifest', async () => {
const projectPath = await createProject('mini_program');
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
tempDirectories.push(outputDirectory);
const archivePath = join(outputDirectory, 'project.zip');
const summary = await createStaticProjectPackage({ projectPath, archivePath });
expect(summary.manifest.project_type).toBe('mini_program');
expect(new AdmZip(archivePath).readAsText('niancode.yml')).toContain('project_type: mini_program\n');
});
it('rejects custom projects before validating Vite source files', async () => {
const projectPath = await mkdtemp(join(tmpdir(), 'makelore-custom-packager-'));
tempDirectories.push(projectPath);
await mkdir(join(projectPath, '.niancode'), { recursive: true });
await writeFile(
join(projectPath, '.niancode', 'project.json'),
JSON.stringify(createProjectConfig('2026-08-09T00:00:00.000Z', 'custom')),
'utf8',
);
const outputDirectory = await mkdtemp(join(tmpdir(), 'makelore-project-archives-'));
tempDirectories.push(outputDirectory);
await expect(createStaticProjectPackage({
projectPath,
archivePath: join(outputDirectory, 'project.zip'),
})).rejects.toMatchObject<ProjectPackageError>({
code: 'PROJECT_TYPE_UNPUBLISHABLE',
message: '自定义项目暂未配置发布方式,请新建小游戏或小程序项目',
});
});
it('rejects a project that is not a locked npm Vite project', async () => {
const projectPath = await createProject();
await writeFile(

View File

@@ -76,7 +76,7 @@ describe('ProjectPublishAction', () => {
it('submits automatic metadata and locks after Builder succeeds', async () => {
fetchCurrentWorksProjectStatusMock.mockResolvedValue(projectStatus('succeeded'));
render(<ProjectPublishAction project={project} />);
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
expect(screen.getByRole('button', { name: '正在检查并提交…' })).toBeDisabled();
@@ -89,7 +89,7 @@ describe('ProjectPublishAction', () => {
title: 'space-cleaner',
summary: 'space-cleaner由 Makelore 创建的作品。',
cover_url: null,
category: 'web',
category: 'mini_game',
age_band: null,
difficulty: null,
},
@@ -109,7 +109,7 @@ describe('ProjectPublishAction', () => {
fetchCurrentWorksProjectStatusMock.mockResolvedValue(
projectStatus('failed', 'BROWSER_SMOKE_FAILED'),
);
render(<ProjectPublishAction project={project} />);
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
@@ -128,7 +128,7 @@ describe('ProjectPublishAction', () => {
fetchCurrentWorksProjectStatusMock.mockRejectedValue(
new Error('socket closed at /srv/private/status.py'),
);
render(<ProjectPublishAction project={project} />);
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
@@ -145,7 +145,7 @@ describe('ProjectPublishAction', () => {
const status = projectStatus('queued');
status.latest_version.id = 'another-version';
fetchCurrentWorksProjectStatusMock.mockResolvedValue(status);
render(<ProjectPublishAction project={project} />);
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
@@ -165,7 +165,7 @@ describe('ProjectPublishAction', () => {
new Error('Traceback: failed at /srv/private/package.ts token=secret'),
{ code: 'INTERNAL_DATABASE_FAILURE', statusCode: 503 },
));
render(<ProjectPublishAction project={project} />);
render(<ProjectPublishAction project={project} projectType="mini_game" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
@@ -182,14 +182,14 @@ describe('ProjectPublishAction', () => {
new Error('D:/repo/space-cleaner/package-lock.json does not exist'),
{ code: 'PROJECT_FILE_MISSING' },
));
render(<ProjectPublishAction project={project} />);
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('请让开发助手补齐 package.json、package-lock.json 和 index.html');
expect(failure).toHaveTextContent('请让开发助手修复当前项目模板');
expect(failure).not.toHaveTextContent('D:/repo');
expect(failure).not.toHaveTextContent('PROJECT_FILE_MISSING');
expect(fetchCurrentWorksProjectStatusMock).not.toHaveBeenCalled();
@@ -197,7 +197,7 @@ describe('ProjectPublishAction', () => {
it('keeps generated metadata within the server limits', async () => {
const longName = `作品-${'x'.repeat(250)}`;
render(<ProjectPublishAction project={{ ...project, name: longName }} />);
render(<ProjectPublishAction project={{ ...project, name: longName }} projectType="mini_program" />);
fireEvent.click(screen.getByRole('button', { name: '一键提交审核' }));
await flushSubmission();
@@ -206,5 +206,6 @@ describe('ProjectPublishAction', () => {
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');
});
});

View File

@@ -232,9 +232,9 @@ describe('Sidebar project initialization flow', () => {
expect(update).toHaveAccessibleName('下载新版本 0.9.2');
});
it('creates a path-backed project without selecting a template and opens configuration', async () => {
it('defaults to a publishable mini-game project and opens configuration', async () => {
const project = { id: 'prj_game', path: 'D:/repo/星星收集器', name: '星星收集器', createdAt: '2026-07-11T00:00:00.000Z', updatedAt: '2026-07-11T00:00:00.000Z', lastOpenedAt: '2026-07-11T00:00:00.000Z' };
const config = createProjectConfig();
const config = createProjectConfig(undefined, 'mini_game');
invokeIpcMock.mockResolvedValue({ canceled: false, filePaths: ['D:/repo/星星收集器'] });
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
@@ -245,19 +245,22 @@ describe('Sidebar project initialization flow', () => {
});
render(<MemoryRouter><Sidebar /></MemoryRouter>);
fireEvent.click(await screen.findByRole('button', { name: '新建项目' }));
expect(screen.getByRole('radio', { name: '小游戏' })).toBeChecked();
expect(screen.getByRole('radio', { name: '小程序' })).not.toBeChecked();
expect(screen.getByRole('radio', { name: '自定义项目' })).not.toBeChecked();
expect(screen.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked();
expect(screen.queryByLabelText('项目名称')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '选择路径' }));
await waitFor(() => expect(screen.getByLabelText('项目路径')).toHaveValue('D:/repo/星星收集器'));
expect(screen.getByText('将直接接入此文件夹,项目名称使用“星星收集器”,不会重命名目录。')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '确认创建' }));
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/create', { method: 'POST', body: JSON.stringify({ projectPath: 'D:/repo/星星收集器' }) }));
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/create', { method: 'POST', body: JSON.stringify({ projectPath: 'D:/repo/星星收集器', projectType: 'mini_game' }) }));
await waitFor(() => expect(navigateMock).toHaveBeenCalledWith('/project-config'));
});
it('keeps creating a named child folder when that directory mode is selected', async () => {
const project = { id: 'prj_child', path: 'D:/repo/星星收集器', name: '星星收集器', createdAt: '', updatedAt: '', lastOpenedAt: '' };
const config = createProjectConfig();
const config = createProjectConfig(undefined, 'mini_program');
invokeIpcMock.mockResolvedValue({ canceled: false, filePaths: ['D:/repo'] });
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
@@ -269,6 +272,7 @@ describe('Sidebar project initialization flow', () => {
render(<MemoryRouter><Sidebar /></MemoryRouter>);
fireEvent.click(await screen.findByRole('button', { name: '新建项目' }));
fireEvent.click(screen.getByRole('radio', { name: '小程序' }));
fireEvent.click(screen.getByRole('radio', { name: '在所选位置新建下级文件夹' }));
fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '星星收集器' } });
fireEvent.click(screen.getByRole('button', { name: '选择路径' }));
@@ -280,6 +284,7 @@ describe('Sidebar project initialization flow', () => {
body: JSON.stringify({
parentPath: 'D:/repo',
projectName: '星星收集器',
projectType: 'mini_program',
}),
}));
});

View File

@@ -6,7 +6,8 @@ import {
describe('works project publish guidance', () => {
it.each([
['PROJECT_FILE_MISSING', '项目文件不完整', 'package-lock.json'],
['PROJECT_FILE_MISSING', '项目文件不完整', '修复当前项目模板'],
['PROJECT_TYPE_UNPUBLISHABLE', '这个项目没有配置发布方式', '新建小游戏或小程序项目'],
['DEPENDENCY_PREFETCH_FAILED', '暂时无法下载项目依赖', 'package-lock.json'],
['OUTPUT_MISSING', '没有生成可运行页面', 'index.html'],
['RELEASE_STORE_FAILED', '平台暂时无法保存发布文件', '无需修改项目'],

View File

@@ -5,6 +5,7 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { handleWorksRoutes } from '@electron/api/routes/works';
import { createProjectConfig } from '../../shared/project-config';
const readWorksPublishFileMock = vi.hoisted(() => vi.fn());
const readWorksDeployCheckMock = vi.hoisted(() => vi.fn());
@@ -57,6 +58,12 @@ function createRequest(method: string, body?: unknown, headers: Record<string, s
async function writePublishableProject(projectPath: string): Promise<void> {
await mkdir(join(projectPath, 'src'), { recursive: true });
await mkdir(join(projectPath, '.niancode'), { recursive: true });
await writeFile(
join(projectPath, '.niancode', 'project.json'),
JSON.stringify(createProjectConfig('2026-08-09T00:00:00.000Z', 'mini_game')),
'utf8',
);
await writeFile(join(projectPath, 'package.json'), JSON.stringify({
name: 'space-cleaner',
private: true,