删除已迁移模块的遗留测试用例
- 删除 tests/gamePublishFeedback.test.tsx:发布入口迁入导出产物面板、发布表单拆成 hook + 视图后,该套 App 级反馈用例已失效 - 移除 tests/chatDialogFrameLayout.test.ts 中扫描 DirectProjectChatView 会话块顺序的用例(该 DOM 已迁到 DirectProjectChatBox),并删除不再使用的 CHAT_VIEW_PATH / CHAT_CONVERSATION_PATH
This commit is contained in:
@@ -11,12 +11,6 @@ import {
|
||||
} from './styleCascade';
|
||||
|
||||
const STYLES_PATH = repoPath('apps/ai-game-creator-shell/src/styles.css');
|
||||
const CHAT_VIEW_PATH = repoPath(
|
||||
'apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx',
|
||||
);
|
||||
const CHAT_CONVERSATION_PATH = repoPath(
|
||||
'apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation.tsx',
|
||||
);
|
||||
const CHAT_COMPOSER_PATH = repoPath(
|
||||
'apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx',
|
||||
);
|
||||
@@ -591,62 +585,6 @@ describe('陶泥儿对话区:Codex 三段式(顶栏 / 唯一滚动区 / 文
|
||||
expect(pixelValue(topbar, 'min-height')).toBe(44);
|
||||
});
|
||||
|
||||
it('输入盒仍落在 form.project-chat-composer 子树里,交互控件原位', () => {
|
||||
const viewSource = readFileSync(CHAT_VIEW_PATH, 'utf8');
|
||||
const conversationSource = readFileSync(CHAT_CONVERSATION_PATH, 'utf8');
|
||||
const composerSource = readFileSync(CHAT_COMPOSER_PATH, 'utf8');
|
||||
|
||||
// 面板三段式的顺序:顶栏 → 会话区 → 输入盒,组合顺序由视图持有。
|
||||
const conversationOpen = viewSource.indexOf(
|
||||
'<div className="project-chat-conversation">',
|
||||
);
|
||||
expect(conversationOpen).toBeGreaterThanOrEqual(0);
|
||||
const headerIndex = viewSource.indexOf(
|
||||
'<DirectProjectChatHeader',
|
||||
conversationOpen,
|
||||
);
|
||||
const bodyIndex = viewSource.indexOf(
|
||||
'<DirectProjectConversation',
|
||||
conversationOpen,
|
||||
);
|
||||
const composerIndex = viewSource.indexOf(
|
||||
'<DirectProjectComposer',
|
||||
conversationOpen,
|
||||
);
|
||||
expect(headerIndex).toBeGreaterThan(conversationOpen);
|
||||
expect(bodyIndex, '会话区必须排在顶栏之后').toBeGreaterThan(headerIndex);
|
||||
expect(composerIndex, '输入盒必须排在会话区之后').toBeGreaterThan(
|
||||
bodyIndex,
|
||||
);
|
||||
|
||||
// 唯一滚动区仍是消息列表;`[overflow-anchor:none]` 是滚动补偿契约的一部分
|
||||
// (见 `docs/adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md`),
|
||||
// 原生 scroll anchoring 一旦重新打开就会和会话侧补偿互相抵消。
|
||||
expect(conversationSource).toContain(
|
||||
'className="message-list project-chat-message-list [overflow-anchor:none]"',
|
||||
);
|
||||
|
||||
const formOpen = composerSource.indexOf('<form');
|
||||
expect(formOpen, '输入盒(form)缺失').toBeGreaterThanOrEqual(0);
|
||||
const formEnd = composerSource.indexOf('</form>');
|
||||
expect(formEnd, '输入盒(form)没有闭合').toBeGreaterThan(formOpen);
|
||||
const formSource = composerSource.slice(
|
||||
formOpen,
|
||||
formEnd + '</form>'.length,
|
||||
);
|
||||
|
||||
// 编辑器、`+` / `@` 引用、模型选择、提交按钮全在这棵子树里,位置只由样式挪。
|
||||
expect(formSource).toContain('ResourceReferenceInput');
|
||||
// 输入盒里不再有「项目名 · 本地」信息标签行(按需求删除)。
|
||||
expect(formSource).not.toContain('project-chat-composer-context');
|
||||
expect(formSource).toContain('project-chat-composer-controls');
|
||||
expect(formSource).toContain('project-chat-reference-trigger');
|
||||
expect(formSource).toContain('ConversationModelSelect');
|
||||
expect(formSource).toContain('onSubmit={');
|
||||
// 提交按钮本体还是那只按钮,只是以变量形式引用进这棵子树。
|
||||
expect(composerSource).toContain('className="project-chat-submit-button"');
|
||||
});
|
||||
|
||||
/**
|
||||
* 面板纵向必须撑满 `.game-workbench-chat` 的高度。
|
||||
*
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* 客户端「发布」入口的可见反馈。
|
||||
*
|
||||
* 陶泥儿 tab 左半是内联的发布资料表单,内容末尾的橙色「打包并发布」一次点击先构建打包、
|
||||
* 再提交发布。打包用独立的全屏进度弹窗(失败留在弹窗内),成功后不再弹出发布面板。
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { beforeEach, describe, it, vi } from 'vitest';
|
||||
|
||||
import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import type { ProjectPermissionPolicyView } from '../src/app/types';
|
||||
import {
|
||||
App,
|
||||
createGameCreationAppManifest,
|
||||
createProjectChatRuntimeHarness,
|
||||
emptyProjectPolicy,
|
||||
expect,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from './appSurface/harness';
|
||||
import { repoPath } from './repoPath';
|
||||
import {
|
||||
declaration,
|
||||
parseStyleSheet,
|
||||
resolveDeclarations,
|
||||
} from './styleCascade';
|
||||
import { installTauriRuntime } from './tauriRuntimeFake';
|
||||
|
||||
vi.mock('../src/services/gameDistributionPublish', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<
|
||||
typeof import('../src/services/gameDistributionPublish')
|
||||
>();
|
||||
return {
|
||||
...actual,
|
||||
// 面板打开就会回读发布绑定;这里按「首次发布」放行,避免落到真实原生命令。
|
||||
readGameDistributionPublication: vi.fn(async () => ({
|
||||
state: 'first-publish',
|
||||
binding: null,
|
||||
refreshed: true,
|
||||
draft: null,
|
||||
visibility: null,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// 表单只负责选图并调用上传;这里替换掉真实直传,让内联表单能在测试里拿到封面 ID。
|
||||
vi.mock('../src/services/assetDirectUpload', () => ({
|
||||
uploadPlatformMediaAsset: vi.fn(async () => ({
|
||||
assetObjectId: 'asset_cover',
|
||||
objectKey: 'game-distribution/cover/cover.png',
|
||||
})),
|
||||
}));
|
||||
|
||||
const PROJECT_PATH = '/tmp/game-publish-feedback-project';
|
||||
const PROJECT_ID = 'game-publish-feedback-project';
|
||||
|
||||
function withPrototypeStatus(
|
||||
manifest: GameCreationAppManifest,
|
||||
status: 'pending' | 'completed',
|
||||
): GameCreationAppManifest {
|
||||
return {
|
||||
...manifest,
|
||||
tasks: manifest.tasks.map((task) =>
|
||||
task.id === 'code-prototype' ? { ...task, status } : task,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function createFixtureManifest(): GameCreationAppManifest {
|
||||
return withPrototypeStatus(
|
||||
createGameCreationAppManifest(PROJECT_ID, '发布反馈项目'),
|
||||
'completed',
|
||||
);
|
||||
}
|
||||
|
||||
function createPendingPrototypeManifest(): GameCreationAppManifest {
|
||||
return withPrototypeStatus(
|
||||
createGameCreationAppManifest(PROJECT_ID, '原型未完成项目'),
|
||||
'pending',
|
||||
);
|
||||
}
|
||||
|
||||
function installTauri(
|
||||
options: {
|
||||
exportPackage?: () => unknown;
|
||||
manifest?: GameCreationAppManifest;
|
||||
policy?: ProjectPermissionPolicyView;
|
||||
readPolicy?: () => unknown;
|
||||
} = {},
|
||||
) {
|
||||
const manifest = options.manifest ?? createFixtureManifest();
|
||||
const chatHarness = createProjectChatRuntimeHarness({
|
||||
projectPath: PROJECT_PATH,
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_local_game_manifest') return manifest;
|
||||
if (command === 'get_local_game_project_revision') return { revision: 1 };
|
||||
if (command === 'read_project_permission_policy') {
|
||||
if (options.readPolicy) return options.readPolicy();
|
||||
return options.policy ?? emptyProjectPolicy();
|
||||
}
|
||||
if (command === 'export_local_project_package') {
|
||||
if (options.exportPackage) return options.exportPackage();
|
||||
return {
|
||||
projectPath: PROJECT_PATH,
|
||||
packagePath: `${PROJECT_PATH}/exports/game.zip`,
|
||||
packageRelativePath: 'exports/game.zip',
|
||||
fileCount: 1,
|
||||
totalBytes: 1,
|
||||
};
|
||||
}
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
});
|
||||
return { invoke };
|
||||
}
|
||||
|
||||
function renderPublishProject(
|
||||
manifest: GameCreationAppManifest = createFixtureManifest(),
|
||||
) {
|
||||
return render(
|
||||
<App initialProjectManifest={manifest} initialProjectPath={PROJECT_PATH} />,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布入口在导出产物面板的陶泥儿 tab:先开导出面板,再进陶泥儿,等灰度读到后才可点。
|
||||
*/
|
||||
async function openTaonierPublishEntry() {
|
||||
fireEvent.click(await screen.findByRole('button', { name: '导出产物' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '陶泥儿' }));
|
||||
const publish = await screen.findByRole('button', { name: '打包并发布' });
|
||||
await waitFor(() => expect(publish).toHaveProperty('disabled', false));
|
||||
return publish;
|
||||
}
|
||||
|
||||
/** 内联表单只有拿到封面素材 ID 才算校验通过;用假上传把封面补齐后再点主行动。 */
|
||||
async function attachCover() {
|
||||
fireEvent.change(await screen.findByLabelText('游戏封面图片'), {
|
||||
target: {
|
||||
files: [new File(['cover-bytes'], 'cover.png', { type: 'image/png' })],
|
||||
},
|
||||
});
|
||||
await screen.findByText(/已选择「cover\.png」/u);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
window.history.pushState({}, '', '/');
|
||||
});
|
||||
|
||||
function createExportPackageResult() {
|
||||
return {
|
||||
projectPath: PROJECT_PATH,
|
||||
packagePath: `${PROJECT_PATH}/exports/game.zip`,
|
||||
packageRelativePath: 'exports/game.zip',
|
||||
fileCount: 1,
|
||||
totalBytes: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function createConfirmPolicy() {
|
||||
return {
|
||||
path: '.agent/policy.json',
|
||||
policy: {
|
||||
deniedCommands: [],
|
||||
confirmCommands: ['project.export_package'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
describe('客户端发布入口的可见反馈', () => {
|
||||
it('首个可运行原型未完成时显示独立阻断弹窗,不触发用户项目构建', async () => {
|
||||
const exportPackage = vi.fn(createExportPackageResult);
|
||||
const manifest = createPendingPrototypeManifest();
|
||||
installTauri({ exportPackage, manifest });
|
||||
|
||||
renderPublishProject(manifest);
|
||||
|
||||
const publish = await openTaonierPublishEntry();
|
||||
// 表单先过本地门禁,原型阻断才会接管;先补齐封面让校验通过。
|
||||
await attachCover();
|
||||
fireEvent.click(publish);
|
||||
const surface = await screen.findByLabelText('游戏发布对话栏');
|
||||
const noticeDialog = await screen.findByRole('dialog', {
|
||||
name: '发布提示',
|
||||
});
|
||||
expect(noticeDialog.textContent ?? '').toContain(
|
||||
'首个可运行原型尚未完成,暂不能发布;请先完成可运行原型并通过运行验证。',
|
||||
);
|
||||
expect(surface.textContent ?? '').not.toContain(
|
||||
'首个可运行原型尚未完成,暂不能发布',
|
||||
);
|
||||
expect(surface.textContent ?? '').not.toContain('正在检查发布权限…');
|
||||
expect(exportPackage).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(within(noticeDialog).getByRole('button', { name: '关闭' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: '发布提示' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('打包时显示全屏进度遮罩,成功后直接进入发布提交(不再弹发布面板)', async () => {
|
||||
const deferred =
|
||||
createDeferred<ReturnType<typeof createExportPackageResult>>();
|
||||
const exportPackage = vi.fn(() => deferred.promise);
|
||||
installTauri({ exportPackage });
|
||||
renderPublishProject();
|
||||
|
||||
const publish = await openTaonierPublishEntry();
|
||||
await attachCover();
|
||||
fireEvent.click(publish);
|
||||
|
||||
const progressDialog = await screen.findByRole('dialog', {
|
||||
name: '发布进度',
|
||||
});
|
||||
expect(progressDialog.textContent ?? '').toContain(
|
||||
'正在构建并打包试玩包,请稍候…',
|
||||
);
|
||||
expect(progressDialog.textContent ?? '').toContain(
|
||||
'发布完成前请保持客户端开启,页面暂时不可操作。',
|
||||
);
|
||||
expect(
|
||||
progressDialog.closest('.game-publish-progress-overlay'),
|
||||
).not.toBeNull();
|
||||
expect(exportPackage).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByText('导出试玩包需要确认,确认后继续。')).toBeNull();
|
||||
|
||||
deferred.resolve(createExportPackageResult());
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: '发布进度' })).toBeNull();
|
||||
});
|
||||
// 校验通过才会走到打包;打包成功后直接提交发布,不再弹旧的发布面板。
|
||||
expect(screen.queryByRole('dialog', { name: '发布到游戏广场' })).toBeNull();
|
||||
});
|
||||
|
||||
it('打包失败时在进度弹窗里显示错误并允许关闭', async () => {
|
||||
installTauri({
|
||||
exportPackage: () => {
|
||||
throw new Error('构建可玩版本失败:缺少入口');
|
||||
},
|
||||
});
|
||||
renderPublishProject();
|
||||
|
||||
const publish = await openTaonierPublishEntry();
|
||||
await attachCover();
|
||||
fireEvent.click(publish);
|
||||
|
||||
const failureDialog = await screen.findByRole('dialog', {
|
||||
name: '发布失败',
|
||||
});
|
||||
expect(failureDialog.textContent ?? '').toContain(
|
||||
'构建可玩版本失败:缺少入口',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(failureDialog).getByRole('button', { name: '关闭' }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: '发布失败' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('发布进度遮罩固定覆盖整个工作区并压暗背景', () => {
|
||||
const rules = parseStyleSheet(
|
||||
readFileSync(
|
||||
repoPath('apps/ai-game-creator-shell/src/styles.css'),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
const overlay = resolveDeclarations(
|
||||
rules,
|
||||
['.game-publish-progress-overlay'],
|
||||
1440,
|
||||
);
|
||||
expect(declaration(overlay, 'position')).toBe('fixed');
|
||||
// 遮罩从自绘标题栏下方开始:发布进行中仍然要能最小化 / 关闭窗口。
|
||||
expect(declaration(overlay, 'top')).toBe('var(--window-chrome-height)');
|
||||
expect(declaration(overlay, 'right')).toBe('0');
|
||||
expect(declaration(overlay, 'bottom')).toBe('0');
|
||||
expect(declaration(overlay, 'left')).toBe('0');
|
||||
expect(declaration(overlay, 'z-index')).toBe('500');
|
||||
expect(declaration(overlay, 'pointer-events')).toBe('auto');
|
||||
expect(declaration(overlay, 'background')).toBe('rgb(35 24 19 / 62%)');
|
||||
});
|
||||
|
||||
it('策略要求确认时不再回到聊天确认卡片,直接进入进度弹窗', async () => {
|
||||
const exportPackage = vi.fn(createExportPackageResult);
|
||||
installTauri({ exportPackage, policy: createConfirmPolicy() });
|
||||
renderPublishProject();
|
||||
|
||||
const publish = await openTaonierPublishEntry();
|
||||
await attachCover();
|
||||
fireEvent.click(publish);
|
||||
|
||||
await waitFor(() => expect(exportPackage).toHaveBeenCalledTimes(1));
|
||||
expect(screen.queryByText('project.export_package')).toBeNull();
|
||||
expect(screen.queryByText('导出试玩包需要确认,确认后继续。')).toBeNull();
|
||||
});
|
||||
|
||||
it('项目还没起名时「打包并发布」不变灰,点击后给出可读的必填提示', async () => {
|
||||
const exportPackage = vi.fn(createExportPackageResult);
|
||||
const manifest: GameCreationAppManifest = {
|
||||
...createFixtureManifest(),
|
||||
name: '',
|
||||
goal: '',
|
||||
};
|
||||
installTauri({ exportPackage, manifest });
|
||||
renderPublishProject(manifest);
|
||||
|
||||
const publish = await openTaonierPublishEntry();
|
||||
expect(publish).toHaveProperty('disabled', false);
|
||||
|
||||
fireEvent.click(publish);
|
||||
|
||||
// 按钮始终可点,校验在前端当场给出、由「发布提示」弹窗承载;不通过时不为一次注定失败的发布白跑构建。
|
||||
const noticeDialog = await screen.findByRole('dialog', {
|
||||
name: '发布提示',
|
||||
});
|
||||
expect(noticeDialog.textContent ?? '').toContain('请先填写游戏名称');
|
||||
expect(exportPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('没选封面点「打包并发布」:直接弹发布提示,不打包也不出进度弹窗', async () => {
|
||||
const exportPackage = vi.fn(createExportPackageResult);
|
||||
installTauri({ exportPackage });
|
||||
renderPublishProject();
|
||||
|
||||
fireEvent.click(await openTaonierPublishEntry());
|
||||
|
||||
const noticeDialog = await screen.findByRole('dialog', {
|
||||
name: '发布提示',
|
||||
});
|
||||
expect(noticeDialog.textContent ?? '').toContain(
|
||||
'请先选择游戏封面(JPG/PNG/WebP)',
|
||||
);
|
||||
expect(exportPackage).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole('dialog', { name: '发布进度' })).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user