按 review 修:动画在途拦截、样式守卫扩到三面板、窄屏差异显式放行、用例不再依赖 cwd

- 在途拦截补齐生成动画:提交记录的「属于哪张资源」从 draftKey 拆成 resourcePath,
  `resourceCanvasResourceEditSubmissionBlocksResource` 被快速编辑与生成动画共用;
  此前动画这条键对不上、拦不住,重开面板再提交会复用同一 operationId 卡在原生锁上,
  或(改了提示词)直接多出一笔付费生成
- 样式缺口守卫扩到三块面板(动画面板 / 快速编辑面板 / 生图面板),判据改为按
  「选择器分支 + 声明」逐条比对;两个面板的实测缺口一并照抄补上(快速编辑面板的
  `overflow: auto → visible` 与窄屏底栏两条按宿主差异显式登记,不静默漂移)
- 窄屏只搬动画面板那一支:网页端 ≤760px 会把快速编辑浮层也变成整宽底栏,AGC 的
  快速编辑是「居中贴资源卡」的绝对定位,跟随它会压住栏目工具栏与任务侧栏
- 用例路径不再依赖 cwd:新增 tests/repoPath.ts(按 import.meta.url 反推仓库根),
  19 个用例文件的 `resolve(process.cwd(), …)` 统一改用它;顺手修掉
  `HOST_STYLE_SHEETS.filter(existsSync)` 把仓库相对路径交给 cwd 解析的假红
- 清掉两处死代码:不可达的 `quickEditPanel.status === 'generating'` 判据与
  未被使用的 prune 导出
This commit is contained in:
2026-09-21 01:23:35 +08:00
parent eed72ba39a
commit 2a2e4c1cea
22 changed files with 332 additions and 202 deletions
@@ -1781,8 +1781,14 @@ button.image-canvas-editor__reference-chip:disabled {
* 两边再各自漂移。
*/
@media (max-width: 760px) {
/* src/index.css:9020 */
.image-canvas-editor__quick-edit-panel,
/*
* src/index.css:9020 —— **只搬动画面板那一支**。
*
* 网页端这一条把快速编辑浮层与动画面板一起变成 `position: fixed` 的整宽底栏;AGC 的快速编辑
* 浮层是「居中贴资源卡」的绝对定位(`transform: translateX(-50%)` + 内联 left/top,见本文件
* 上面那一族),窄屏宽度已经由 `calc(100vw - 1.5rem)` 自己收好——跟网页端改成底栏会压住栏目
* 画布左下角工具栏与左侧「生成任务」侧栏。这条宿主差异在样式守卫里显式放行,不静默漂移。
*/
.image-canvas-editor__character-animation-panel {
position: fixed;
left: 0.75rem !important;
@@ -33,12 +33,15 @@ export type ResourceCanvasResourceEditSubmission = {
/** 入口文案:快速编辑 / 生成动画 / 图片抠图…,由 `pendingResourceEditKindLabel` 出。 */
actionLabel: string;
/**
* 快速编辑草稿键(资源路径);其它入口为 `null`。
* 这笔提交属于哪张资源(资源路径,见 `ProjectResource.path`);无源入口(视频 / 音效 /
* 背景音乐)为 `null`。
*
* 提交期间这一笔草稿不再算「未完成编辑」——用户已经交出去了,恢复入口里不该再多一条
* 点开就重复提交的条目(见 `resourceCanvasQuickEditModel`
* 两个用途:① 快速编辑提交期间,这张资源的未提交草稿不再算「未完成编辑」——用户已经交出去了,
* 恢复入口里不该再多一条点开就重复提交的条目(见 `resourceCanvasQuickEditModel`;② 同一张
* 资源还有在途提交时,快速编辑与生成动画都不允许再铸一次身份(见
* `resourceCanvasResourceEditSubmissionBlocksResource`)。
*/
draftKey: string | null;
resourcePath: string | null;
createdAtMillis: number;
status: 'running' | 'completed' | 'failed';
error: string | null;
@@ -163,14 +166,20 @@ export function resourceCanvasResourceEditSubmissionIsLive(
return submission.status === 'running';
}
/** 收口判据:这个草稿键上还有一笔没结束的提交。 */
export function resourceCanvasResourceEditSubmissionBlocksDraft(
/**
* 「这张资源上还有一笔没结束的提交」。
*
* 快速编辑(顺带把该资源的草稿从「未完成编辑」里让位)与生成动画共用同一条判据:提交即关面板
* 之后,「重开面板再提交」是唯一的重试路径,不拦就会出现两条并发提交——提示词没变时第二次会卡在
* 原生 per-operation 锁上、最后以阶段不可恢复报错;改了提示词则直接多出一笔付费生成。
*/
export function resourceCanvasResourceEditSubmissionBlocksResource(
submissions: readonly ResourceCanvasResourceEditSubmission[],
draftKey: string,
resourcePath: string,
): boolean {
return submissions.some(
(submission) =>
submission.draftKey === draftKey &&
submission.resourcePath === resourcePath &&
resourceCanvasResourceEditSubmissionIsLive(submission),
);
}
@@ -232,13 +241,6 @@ export function settleResourceCanvasResourceEditSubmission(
);
}
/** 已结束的提交记录没有继续留着的价值:只保留在途的那几笔。 */
export function pruneResourceCanvasResourceEditSubmissions(
submissions: readonly ResourceCanvasResourceEditSubmission[],
): ResourceCanvasResourceEditSubmission[] {
return submissions.filter(resourceCanvasResourceEditSubmissionIsLive);
}
function resourceCanvasResourceEditTaskFromPendingEdit(
edit: PendingLocalProjectResourceEdit,
): ResourceCanvasResourceEditTask {
@@ -223,7 +223,7 @@ import {
pendingResourceEditKindLabel,
RESOURCE_CANVAS_RESOURCE_EDIT_PHASE_LABELS,
type ResourceCanvasResourceEditSubmission,
resourceCanvasResourceEditSubmissionBlocksDraft,
resourceCanvasResourceEditSubmissionBlocksResource,
resourceCanvasResourceEditTaskIsTerminal,
resourceCanvasResourceEditTasks,
settleResourceCanvasResourceEditSubmission,
@@ -4912,7 +4912,7 @@ export default function ProjectDevelopmentView({
assetName: string;
actionLabel: string;
prompt: string;
draftKey: string | null;
resourcePath: string | null;
}) => {
setResourceEditSubmissions((current) =>
beginResourceCanvasResourceEditSubmission(current, {
@@ -7582,7 +7582,7 @@ export default function ProjectDevelopmentView({
canvasResources,
).filter(
(entry) =>
!resourceCanvasResourceEditSubmissionBlocksDraft(
!resourceCanvasResourceEditSubmissionBlocksResource(
resourceEditSubmissions,
entry.key,
),
@@ -7756,7 +7756,7 @@ export default function ProjectDevelopmentView({
稿
*/
if (
resourceCanvasResourceEditSubmissionBlocksDraft(
resourceCanvasResourceEditSubmissionBlocksResource(
resourceEditSubmissionsRef.current,
resource.path,
)
@@ -7852,7 +7852,7 @@ export default function ProjectDevelopmentView({
assetName: derivedName,
actionLabel: '快速编辑',
prompt,
draftKey: resource.path,
resourcePath: resource.path,
});
try {
const { sourceResourceId, sourceAssetId, expectedProjectRevision } =
@@ -8086,6 +8086,24 @@ export default function ProjectDevelopmentView({
const actionProject = { projectPath, projectId: manifest.projectId };
const flowId = crypto.randomUUID();
const sourceLayerId = layer.id;
// 与快速编辑共用同一条在途判据:这张素材上还有没结束的提交就不再铸一次身份。
if (
resourceCanvasResourceEditSubmissionBlocksResource(
resourceEditSubmissionsRef.current,
resource.path,
)
) {
setCharacterAnimationPanel((current) =>
current
? {
...current,
status: 'failed',
errorMessage: '这张素材正在生成动画,请等它出结果后再提交',
}
: current,
);
return;
}
const requestStoreKey = resourceEditRequestKey(
'character-animation',
resource.path,
@@ -8116,7 +8134,7 @@ export default function ProjectDevelopmentView({
assetName: derivedName,
actionLabel: '生成动画',
prompt,
draftKey: null,
resourcePath: resource.path,
});
try {
const { sourceResourceId, sourceAssetId, expectedProjectRevision } =
@@ -8257,7 +8275,8 @@ export default function ProjectDevelopmentView({
assetName: input.assetName,
actionLabel: option.generationLabel,
prompt: input.prompt,
draftKey: null,
// 无源生成:没有对应的资源卡,因此不参与「同一张资源不许并发提交」那条判据。
resourcePath: null,
});
try {
const status = await invoke<{ revision: number }>(
@@ -10018,9 +10037,6 @@ export default function ProjectDevelopmentView({
multiline
rows={3}
placeholder="你希望素材如何修改?"
disabled={
quickEditPanel.status === 'generating'
}
// 润色由下面的 ResourcePromptPolishSlot 承担
// (带资源编辑场景上下文与长度上限),
// 不在这里再渲染第二个润色入口。
@@ -10033,9 +10049,6 @@ export default function ProjectDevelopmentView({
subject="图片素材的快速编辑提示词"
editKind="image-reference"
prompt={quickEditPanel.prompt}
disabled={
quickEditPanel.status === 'generating'
}
applyPrompt={applyResourceQuickEditPrompt}
/>
}
@@ -1,13 +1,12 @@
// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { render } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { AgentMessageContent } from '../../../packages/shared/src/components/AgentMessageContent';
import { ChatMarkdownMessage } from '../src/components/ChatMarkdownMessage';
import { repoPath } from './repoPath';
import {
declaration,
parseStyleSheet,
@@ -15,14 +14,11 @@ import {
} from './styleCascade';
const sharedCss = readFileSync(
resolve(
process.cwd(),
'packages/shared/src/components/AgentMessageContent.css',
),
repoPath('packages/shared/src/components/AgentMessageContent.css'),
'utf8',
);
const appCss = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const base = '.agent-message-content[data-agent-content]';
@@ -1,6 +1,5 @@
/** @vitest-environment jsdom */
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
act,
@@ -28,6 +27,7 @@ import {
ResourceDependencyOverlay,
type ResourceDependencyOverlayHandle,
} from '../src/view/project-development/ResourceDependencyOverlay';
import { repoPath } from './repoPath';
function position(
resourceId: string,
@@ -1030,7 +1030,7 @@ describe('ResourceDependencyOverlay', () => {
it('uses a persistent orange with at least 3:1 canvas contrast', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
expect(styles).toMatch(
@@ -25,6 +25,7 @@ import { normalizeProjectResourceGraph } from '../../src/view/project-developmen
import { ResourceDependencyOverlay } from '../../src/view/project-development/ResourceDependencyOverlay';
import type { ProjectAgentResultSummary } from '../../src/view/project-development/resourceProjectionModel';
import { projectResourcesFromReadModels } from '../../src/view/project-development/resourceProjectionModel';
import { repoPath } from '../repoPath';
import {
generationPromptText,
typeGenerationPrompt,
@@ -60,7 +61,6 @@ import {
render,
renderAppAt,
renderLauncherProjectsAt,
resolve,
screen,
setComposerText,
submitChat,
@@ -1473,12 +1473,11 @@ export function registerProjectWorkbenchFoundationTests() {
it('keeps the resource filter panel above the titlebar band and anchored to the dock', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const tsxSource = readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/view/project-development/index.tsx',
),
'utf8',
@@ -1556,7 +1555,7 @@ export function registerProjectWorkbenchFoundationTests() {
it('keeps the resource overview grid off fit-content so its columns stay responsive', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
// 先剥掉 CSS 注释再取声明体:这条修复的注释里就写着 `width: 100%` 等字样,
@@ -1686,7 +1685,7 @@ export function registerProjectWorkbenchFoundationTests() {
it('keeps the shared selection overlay colours resolvable from the scene root', () => {
const sharedStyles = readFileSync(
resolve(process.cwd(), 'packages/image-canvas-react/src/styles.css'),
repoPath('packages/image-canvas-react/src/styles.css'),
'utf8',
);
// 消费端 `border/background: var(...)` 没有 fallbacktoken 缺失或被写透明,框选就没有颜色。
@@ -1914,12 +1913,11 @@ export function registerProjectWorkbenchFoundationTests() {
it('keeps the overview-return button out of the floating notice layer hit region', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const tsxSource = readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/view/project-development/index.tsx',
),
'utf8',
@@ -3497,7 +3495,7 @@ export function registerProjectWorkbenchFoundationTests() {
it('资源卡 chrome 只保留选中按钮与当前版本边框', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
/*
@@ -3591,7 +3589,7 @@ export function registerProjectWorkbenchFoundationTests() {
it('资源卡的 @ 引用入口只留在选中工具条里', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
// 卡片右上角那个圆钮连同它撑出来的 44×44 热区一起退役:卡片本体只剩
@@ -3599,8 +3597,7 @@ export function registerProjectWorkbenchFoundationTests() {
expect(styles).not.toContain('.game-resource-card-reference');
const source = readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/view/project-development/index.tsx',
),
'utf8',
@@ -5855,7 +5852,7 @@ export function registerProjectWorkbenchFoundationTests() {
it('keeps the landscape workbench edge-to-edge with internal chat scrolling', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
@@ -6011,12 +6008,11 @@ export function registerProjectWorkbenchFoundationTests() {
it('keeps the wallet entry available when the workbench opens the UI editor', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const projectDevelopmentSource = readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/view/project-development/index.tsx',
),
'utf8',
@@ -6325,7 +6321,7 @@ export function registerProjectWorkbenchFoundationTests() {
it('keeps resource sort tab keyboard focus inside the clipped segmented control', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
@@ -6336,7 +6332,7 @@ export function registerProjectWorkbenchFoundationTests() {
it('keeps the chat composer an inset block inside the conversation dialog', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
// 对话框就是消息列表那只铺满会话区的盒子;输入区是它内部的一块,不再是贴在它下边、
@@ -6385,7 +6381,7 @@ export function registerProjectWorkbenchFoundationTests() {
it('keeps workbench chat bubbles aligned without shrinking process cards', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const messageListRule =
@@ -7097,7 +7093,7 @@ export function registerProjectSupervisorSurfaceTests() {
it('anchors the model dropdown to its own trigger instead of the composer box', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
@@ -7157,7 +7153,7 @@ export function registerProjectSupervisorSurfaceTests() {
it('bounds the model dropdown height so a long catalog cannot cover the composer', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const menu = styleRuleBody(styles, '\\.conversation-model-menu');
@@ -11657,7 +11653,7 @@ export function registerProjectAgentStatusTests() {
).not.toBeNull();
// 宿主几何与栏目卡共用同一条规则,视觉上不是另写一套。
const previewStyles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const previewHostRule = styleRuleBody(
@@ -11765,7 +11761,7 @@ export function registerProjectAgentStatusTests() {
// 视觉不是另写一套:那一页没有任何专属选择器,卡与宿主都只用共享规则
// (声明级断言——它验的是"没有平行样式",验不到布局本身)。
const allPageStyles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
expect(allPageStyles).not.toMatch(/\.game-resource-all-/);
@@ -13394,12 +13390,11 @@ export function registerProjectAgentStatusTests() {
it('keeps the bottom toolbar clear of the zoom dock and above the book scene', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const chromeStyles = readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css',
),
'utf8',
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { APP_VERSION } from '../../src/app/appMetadata';
import { repoPath } from '../repoPath';
import {
act,
createGameCreationAppManifest,
@@ -23,7 +23,7 @@ import {
export function registerAgentStatusDerivationTests() {
it('keeps fixed overlays below the in-page window title bar', () => {
const styles = fs.readFileSync(
path.join(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
expect(styles).toContain('--window-chrome-height: 50px;');
@@ -1,22 +1,17 @@
// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { repoPath } from './repoPath';
import {
declaration,
parseStyleSheet,
resolveDeclarations,
} from './styleCascade';
const STYLES_PATH = resolve(
process.cwd(),
'apps/ai-game-creator-shell/src/styles.css',
);
const VIEW_PATH = resolve(
process.cwd(),
const STYLES_PATH = repoPath('apps/ai-game-creator-shell/src/styles.css');
const VIEW_PATH = repoPath(
'apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx',
);
@@ -7,6 +7,7 @@ import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { ImageCanvasProjectAssetPickerDialog } from '../../../src/components/image-editor/ImageCanvasProjectAssetPickerDialog';
import { REPO_ROOT } from './repoPath';
/**
* 「选择替换素材」弹窗在 AGC 的面板底色契约。
@@ -25,7 +26,6 @@ import { ImageCanvasProjectAssetPickerDialog } from '../../../src/components/ima
* 这是**弱验证**(声明级,不是真机观感)。真机判据见文件末尾注释。
*/
const REPO_ROOT = process.cwd();
const AGC_ROOT = resolve(REPO_ROOT, 'apps/ai-game-creator-shell');
const PLATFORM_MODAL_SHELL = '.platform-modal-shell';
@@ -336,9 +336,14 @@ describe('「选择替换素材」弹窗在 AGC 的面板底色', () => {
});
it('没有平行拷贝:外壳三条规则只在共享表里定义一次', () => {
const hostRules = HOST_STYLE_SHEETS.filter(existsSync).flatMap((sheet) =>
readRules(absoluteSheetPath(sheet)),
);
/*
这里必须**先转绝对路径再判断存在**`filter(existsSync)` 拿到的是仓库相对路径,`existsSync`
按 `process.cwd()` 解析——从 `apps/ai-game-creator-shell` 跑时四张表全被判成不存在,
断言退化成「谁都没定义」的假绿/假红(本次就是从这条假红里揪出来的)。
*/
const hostRules = HOST_STYLE_SHEETS.filter((sheet) =>
existsSync(absoluteSheetPath(sheet)),
).flatMap((sheet) => readRules(absoluteSheetPath(sheet)));
for (const className of [
PLATFORM_MODAL_SHELL,
PLATFORM_MODAL_BACKDROP,
@@ -0,0 +1,23 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
/**
* 仓库根与"按仓库相对路径取文件"的唯一入口。
*
* 读样式表 / 脚本 / 文档的用例原先一律写 `resolve(process.cwd(), 'apps/ai-game-creator-shell/…')`
* 从仓库根跑(CI 的 `npm run test`、根 `vitest.config.ts`)没问题,但只要从
* `apps/ai-game-creator-shell` 目录跑一次,路径就会翻成
* `…/apps/ai-game-creator-shell/apps/ai-game-creator-shell/…` 并整片 ENOENT——看起来像"这些用例本来
* 就红",实际是路径解析跟着 cwd 漂。这里按 `import.meta.url` 反推仓库根,两种跑法结论一致。
*/
export const REPO_ROOT = resolve(
dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'..',
);
/** 传仓库相对路径(如 `apps/ai-game-creator-shell/src/styles.css`)。 */
export function repoPath(...segments: readonly string[]): string {
return resolve(REPO_ROOT, ...segments);
}
@@ -1,9 +1,9 @@
// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, test } from 'vitest';
import { repoPath } from './repoPath';
import {
declaration,
parseStyleSheet,
@@ -17,8 +17,7 @@ import {
* jsdom 不加载这个 CSS 文件,所以这里按仓库既有做法(`styleCascade`)直接解析**真实生效的声明**:
* 「状态 tone 映射」「等宽数字」「圆角 / 悬停」「过渡」「reduced-motion 关动效」都用声明钉住。
*/
const SIDEBAR_CSS_PATH = resolve(
process.cwd(),
const SIDEBAR_CSS_PATH = repoPath(
'apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTasksSidebar.css',
);
const rules = parseStyleSheet(readFileSync(SIDEBAR_CSS_PATH, 'utf8'));
@@ -1,6 +1,5 @@
/** @vitest-environment jsdom */
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
@@ -17,6 +16,7 @@ import {
waitFor,
within,
} from './appSurface/harness';
import { repoPath } from './repoPath';
vi.mock('@tauri-apps/api/core', async () => ({
...(await vi.importActual<typeof import('@tauri-apps/api/core')>(
@@ -237,7 +237,7 @@ function renderWorkbench(manifest: GameCreationAppManifest) {
function stylesSource() {
return readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
}
@@ -12,25 +12,51 @@ import { ImageCanvasCharacterAnimationPanelView } from '../../../src/components/
import type {
CanvasLayer,
CharacterAnimationPanelState,
QuickEditPanelState,
} from '../../../src/components/image-editor/ImageCanvasEditorTypes';
import { ImageCanvasQuickEditPanelView } from '../../../src/components/image-editor/ImageCanvasQuickEditPanelView';
import { ResourceCanvasAssetGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView';
import type { ResourceCanvasAssetToolAction } from '../src/features/resource-canvas/resourceCanvasBottomToolbarModel';
import { createResourceQuickEditPanelDraft } from '../src/features/resource-canvas/resourceCanvasQuickEditModel';
/**
* 「生成动画」面板在 AGC 的样式覆盖契约。
* 共享画布面板在 AGC 的样式覆盖契约(动画面板 / 快速编辑面板 / 生图面板)
*
* 现象(客户端验收现场):资源卡上点「生成动画」面板只剩通用 composer 外观——素材缩略图、
* 动作预设、`生成NN泥点` 按钮与关闭键全部落回默认流布局,面板里出现一大块空白、预设竖排。
* 根因是这套 `image-canvas-editor__character-animation-*` / `__generation-close` /
* `__reference-*` 规则只写在网页端整站表 `src/index.css` 里,而 AGC 是独立宿主
* 现象(客户端验收现场,两次):①「生成动画」面板只剩通用 composer 外观——素材缩略图、动作预设、
* `生成NN泥点` 按钮与关闭键全部落回默认流布局,关闭键还跑到面板正中;② 快速编辑 composer 的同族
* 样式也缺过。根因是这套 `image-canvas-editor__*` 规则只写在网页端整站表 `src/index.css` 里,
* 而 AGC 是独立宿主,宿主的 `resourceCanvasChrome.css` 只能逐条照抄
*
* jsdom 不计算外部 CSS,所以这里不做"渲染出来看观感",而是把两半分别钉死:
* 1) DOM 契约:面板真的渲染出这些类名(含 `reference-chip--${tone}` 这种运行时拼出来的);
* 2) 样式表契约:从 AGC 入口 `src/main.tsx` 按真实 import 关系解析出"AGC 到底加载了哪些
* 样式表",断言上面每个类都至少命中一条规则,并抽查几条撑开布局的关键声明
* 1) DOM 契约:真渲染出这三块面板(含 `reference-chip--${tone}` 这种运行时拼出来的);
* 2) 样式表契约:从 AGC 入口 `src/main.tsx` 按真实 import 关系解析出"AGC 到底加载了哪些样式表"
* 逐条比对**网页端命中这份 DOM 的规则(选择器分支 + 声明)AGC 是否都有一条**
*
* 这是**弱验证**(声明级,不是真机观感)。真机判据:资源卡 → 生成动画 → 缩略图、五个动作
* 预设一排、按钮落在面板右下角、右上角有关闭键;窄屏(≤720px)面板落到底部整宽、按钮整宽。
* 比对必须按「选择器分支 + 声明」而不是类名或整串选择器:同名规则在网页端会出现两次
* `__generation-close` 先给基础外观、再给浮层内的绝对定位),只搬第一条照样"类名有规则"却跑偏;
* 而 `A, B { … }` 里只命中 A 的那一支,也不该逼宿主连 B 一起搬。
*
* 这是**弱验证**(声明级,不是真机观感)。真机判据:资源卡 → 生成动画 → 缩略图、动作预设一排、
* 按钮落在面板右下角、右上角有关闭键;窄屏动画面板落到底部整宽、按钮整宽。
*/
/**
* 显式放行的宿主差异(每条都写明原因,避免"守卫红了就加白名单"式的静默漂移):
*
* 1. 快速编辑浮层**窄屏**:网页端在 `@media (max-width: 760px)` 把它与动画面板一起变成
* `position: fixed` 的整宽底栏(下一条是配套的 bottom 计算)。AGC 的快速编辑浮层是
* 「居中贴资源卡」的绝对定位(`transform: translateX(-50%)` + 内联 left/top),窄屏宽度由
* `calc(100vw - 1.5rem)` 自己收;改成底栏会压住栏目画布左下角工具栏与左侧「生成任务」侧栏。
* 2. 快速编辑浮层**桌面档**:网页端 `overflow: auto`AGC 必须 `overflow: visible`——AGC 没有
* portal,共享组件的下拉弹层就地渲染,面板一旦滚动就会把弹层裁掉(见 `resourceCanvasChrome.css`
* 快速编辑面板那段注释)。
*/
const ACCEPTED_AGENT_HOST_DIVERGENCES = new Set([
'.image-canvas-editor__quick-edit-panel { position: fixed; left: 0.75rem !important; right: 0.75rem; top: auto !important; bottom: 0.75rem; width: auto; max-height: min(72vh, 34rem); transform: none; }',
'.image-canvas-editor__quick-edit-panel.image-canvas-editor__generation-composer { width: auto; bottom: calc(9rem + env(safe-area-inset-bottom, 0px)); }',
'.image-canvas-editor__quick-edit-panel { position: absolute; z-index: 14; display: grid; width: min(24rem, calc(100vw - 1.5rem)); max-height: min(32rem, calc(100% - 1.5rem)); gap: 0.58rem; overflow: auto; border: 1px solid rgba(148, 163, 184, 0.36); border-radius: 1.1rem; background: rgba(255, 255, 255, 0.96); padding: 0.72rem; color: #1f2937; box-shadow: 0 22px 48px rgba(15, 23, 42, 0.22); transform: translateX(-50%); }',
]);
const TEST_DIR = dirname(fileURLToPath(import.meta.url));
const AGC_ROOT = resolve(TEST_DIR, '..');
const REPO_ROOT = resolve(AGC_ROOT, '..', '..');
@@ -215,46 +241,149 @@ function animationPanel() {
return container;
}
function quickEditPanel() {
const sourceLayer: CanvasLayer = {
id: 'layer-q',
resourceId: 'resource-q',
title: 'source-art.png',
src: 'data:image/png;base64,c291cmNl',
x: 0,
y: 0,
width: 128,
height: 128,
originalWidth: 128,
originalHeight: 128,
zIndex: 1,
sourceType: 'uploaded',
};
function Harness() {
const [panel, setPanel] = useState<QuickEditPanelState | null>(() =>
createResourceQuickEditPanelDraft(sourceLayer),
);
return panel ? (
<ImageCanvasQuickEditPanelView
panel={panel}
style={{ left: 12, top: 24 }}
setQuickEditPanel={setPanel}
onSubmit={vi.fn()}
/>
) : null;
}
const { container } = render(<Harness />);
return container;
}
function assetGenerationPanel() {
const action: ResourceCanvasAssetToolAction = {
route: 'asset',
audioKind: null,
id: 'generate-image',
label: '生成图片',
assetKind: 'image',
assetName: '素材 生成图片',
promptPlaceholder: '描述要生成什么',
adjustableDimensions: true,
aspectRatio: '1:1',
imageSize: '1K',
requiresIconSpecReference: false,
writesIconSpecReference: false,
};
const referenceAsset: GameCreationAppAssetManifestEntry = {
id: 'asset-a',
kind: 'image',
mediaType: 'image/png',
localPath: 'assets/素材-a.png',
source: { kind: 'canvas' },
};
const { container } = render(
<ResourceCanvasAssetGenerationPanelView
action={action}
assets={[referenceAsset]}
projectPath="/tmp/project"
draft={{
prompt: '',
assetName: '猫',
aspectRatio: '1:1',
imageSize: '1K',
references: [],
}}
onSubmit={vi.fn()}
onClose={() => undefined}
/>,
);
return container;
}
afterEach(() => {
cleanup();
});
describe('「生成动画」面板的 AGC 样式覆盖', () => {
it('网页端给了规则的类,AGC 也必须有一条(样式缺口守卫)', () => {
const container = animationPanel();
const classes = new Set<string>();
for (const element of container.querySelectorAll('[class]')) {
for (const name of element.classList) {
if (name.startsWith('image-canvas-editor__')) {
classes.add(name);
}
}
}
expect(classes.size).toBeGreaterThan(8);
/** 判据只认「网页端有、AGC 没有」这一类:两边都没有的类是按设计不带样式(如纯语义 span)。 */
const agcSelectors = collectAgcLoadedStyleSheets()
/**
* 网页端命中这份 DOM、但 AGC 一条都没有的(选择器分支 + 声明)。
*
* 比对键是**分支 + 声明**而不是类名或整串选择器:同名规则在网页端会出现两次,只搬第一条也
* "类名有规则"却跑偏;`A, B { … }` 里只命中 A 的那一支,也不该逼宿主连 B 一起搬。
*/
function missingWebRulesFor(container: HTMLElement): {
missing: string[];
matchedCount: number;
} {
const elements = [container, ...Array.from(container.querySelectorAll('*'))];
const matched = readRules(resolve(REPO_ROOT, 'src/index.css')).flatMap(
(rule) =>
rule.selector
.split(',')
.map((selector) => selector.trim().replace(/\s+/gu, ' '))
.filter(
(selector) =>
selector.includes('image-canvas-editor__') &&
ruleMatchesPanel(selector, elements),
)
.map((selector) => `${selector} { ${rule.body} }`),
);
const agcRuleKeys = new Set(
collectAgcLoadedStyleSheets()
.flatMap(readRules)
.map((rule) => rule.selector)
.join('\n');
const webSelectors = readRules(resolve(REPO_ROOT, 'src/index.css'))
.map((rule) => rule.selector)
.join('\n');
const webStyled = [...classes].filter((name) =>
webSelectors.includes(`.${name}`),
);
expect(
webStyled.length,
'网页端样式这一次一条都没匹配上:断言退化成空集,守卫失效',
).toBeGreaterThan(8);
const missing = webStyled.filter(
(name) => !agcSelectors.includes(`.${name}`),
);
expect(
missing,
`这些类网页端有规则、AGC 一条都没有:${missing.join(', ')}`,
).toEqual([]);
});
.flatMap((rule) =>
rule.selector
.split(',')
.map(
(selector) =>
`${selector.trim().replace(/\s+/gu, ' ')} { ${rule.body} }`,
),
),
);
return {
missing: [
...new Set(
matched.filter(
(key) =>
!agcRuleKeys.has(key) && !ACCEPTED_AGENT_HOST_DIVERGENCES.has(key),
),
),
],
matchedCount: matched.length,
};
}
describe('共享画布面板的 AGC 样式覆盖', () => {
for (const probe of [
{ name: '「生成动画」面板', render: animationPanel },
{ name: '快速编辑面板', render: quickEditPanel },
{ name: '生图(资源生成)面板', render: assetGenerationPanel },
]) {
it(`${probe.name}AGC `, () => {
const { missing, matchedCount } = missingWebRulesFor(probe.render());
expect(
matchedCount,
`${probe.name}退`,
).toBeGreaterThan(0);
expect(
missing,
`${probe.name}AGC \n${missing.join('\n')}`,
).toEqual([]);
});
}
it('撑开面板布局的关键声明逐条在位', () => {
const rules = collectAgcLoadedStyleSheets().flatMap(readRules);
@@ -311,17 +440,12 @@ describe('「生成动画」面板的 AGC 样式覆盖', () => {
).toBe('3.95rem');
});
it('网页端命中这块面板的每条规则,AGC 都照抄了一条同名规则', () => {
it('判据本身可信:动画面板至少要匹配上十几条网页端规则', () => {
const container = animationPanel();
const elements = [
container,
...Array.from(container.querySelectorAll('*')),
];
/*
只守画布组件这一族(`image-canvas-editor__*`):共享 chrome 类(`.platform-button` 等)
与全局 reset 本来就不归宿主 chrome 表照抄——它们活在共享样式表里,宿主引入与否是另一件事。
*/
const matched = readRules(resolve(REPO_ROOT, 'src/index.css')).filter(
(rule) =>
rule.selector.includes('image-canvas-editor__') &&
@@ -329,28 +453,6 @@ describe('「生成动画」面板的 AGC 样式覆盖', () => {
.split(',')
.some((selector) => ruleMatchesPanel(selector, elements)),
);
expect(
matched.length,
'一条规则都没匹配上:判据退化成空集,守卫失效',
).toBeGreaterThan(10);
/*
比对键是**选择器 + 声明**而不是选择器:同名规则在网页端出现两次时(例如
`.image-canvas-editor__generation-close` 先给基础外观、再给浮层内的绝对定位),
只按选择器比对会让「只搬了第一条」判成已覆盖——这正是关闭键掉进网格流、跑到面板正中的原因。
*/
const ruleKey = (rule: CssRule) => `${rule.selector} { ${rule.body} }`;
const agcRuleKeys = new Set(
collectAgcLoadedStyleSheets().flatMap(readRules).map(ruleKey),
);
const missing = [
...new Set(
matched.filter((rule) => !agcRuleKeys.has(ruleKey(rule))).map(ruleKey),
),
];
expect(
missing,
`这些规则网页端会命中动画面板、AGC 一条都没有:\n${missing.join('\n')}`,
).toEqual([]);
expect(matched.length).toBeGreaterThan(10);
});
});
@@ -1,6 +1,5 @@
// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
@@ -9,6 +8,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { ResourceCanvasAssetGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView';
import type { ResourceCanvasAssetToolAction } from '../src/features/resource-canvas/resourceCanvasBottomToolbarModel';
import { ResourceCanvasGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasGenerationPanelView';
import { repoPath } from './repoPath';
afterEach(cleanup);
@@ -112,8 +112,7 @@ describe('生成浮层的面板外观与高度合同', () => {
describe('生成浮层样式结构', () => {
const panelCss = () =>
readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationPanel.css',
),
'utf8',
@@ -1,7 +1,6 @@
/** @vitest-environment jsdom */
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
@@ -14,6 +13,7 @@ import {
screen,
waitFor,
} from './appSurface/harness';
import { repoPath } from './repoPath';
/**
* 「生成任务」侧栏与工具条入口的三条口径:
@@ -144,7 +144,7 @@ describe('「生成任务」侧栏的开合与入口位置', () => {
it('「依赖 / 类型」与前一按钮之间的间距跟行内其他按钮一致', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
// 这一行是 gap: 7px 的 flex 容器;分段控件曾靠自带 padding: 3px 与前一按钮只隔 10px
@@ -157,7 +157,7 @@ describe('「生成任务」侧栏的开合与入口位置', () => {
it('布局状态提示不参与动作行排版,不会按文案宽度顶开按钮', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
// 它是随保存过程变长的文案(空 →「保存中」→「布局已保存」→ 失败原因)。作为 flex 子项
@@ -6,7 +6,7 @@ import {
pendingResourceEditKindLabel,
resourceCanvasResourceEditPhaseLabel,
type ResourceCanvasResourceEditSubmission,
resourceCanvasResourceEditSubmissionBlocksDraft,
resourceCanvasResourceEditSubmissionBlocksResource,
resourceCanvasResourceEditTaskElapsedMillis,
resourceCanvasResourceEditTaskIsTerminal,
resourceCanvasResourceEditTasks,
@@ -38,7 +38,7 @@ function liveSubmission(
prompt: '把嘴改小一点',
assetName: 'source-art-编辑版',
actionLabel: '快速编辑',
draftKey: 'assets/source-art.png',
resourcePath: 'assets/source-art.png',
createdAtMillis: 1_700_000_000_000,
status: 'running',
error: null,
@@ -120,7 +120,7 @@ describe('资源派生任务:本地提交记录', () => {
assetName: 'source-art-编辑版',
actionLabel: '快速编辑',
prompt: '把嘴改小一点',
draftKey: 'assets/source-art.png',
resourcePath: 'assets/source-art.png',
createdAtMillis: 1_700_000_000_000,
});
expect(begun).toHaveLength(1);
@@ -131,7 +131,7 @@ describe('资源派生任务:本地提交记录', () => {
assetName: 'source-art-编辑版',
actionLabel: '快速编辑',
prompt: '把嘴改小一点',
draftKey: 'assets/source-art.png',
resourcePath: 'assets/source-art.png',
createdAtMillis: 1_700_000_001_000,
});
expect(rebegun).toHaveLength(1);
@@ -158,18 +158,21 @@ describe('资源派生任务:本地提交记录', () => {
assetName: 'source-art-编辑版',
actionLabel: '快速编辑',
prompt: '把嘴改小一点',
draftKey: 'assets/source-art.png',
resourcePath: 'assets/source-art.png',
createdAtMillis: 1_700_000_000_000,
});
expect(
resourceCanvasResourceEditSubmissionBlocksDraft(
resourceCanvasResourceEditSubmissionBlocksResource(
live,
'assets/source-art.png',
),
).toBe(true);
// 别的素材不受影响,其它入口(没有草稿键)也不受这条判据牵制。
expect(
resourceCanvasResourceEditSubmissionBlocksDraft(live, 'assets/other.png'),
resourceCanvasResourceEditSubmissionBlocksResource(
live,
'assets/other.png',
),
).toBe(false);
const failed = settleResourceCanvasResourceEditSubmission(live, 'op-1', {
@@ -178,7 +181,7 @@ describe('资源派生任务:本地提交记录', () => {
finishedAtMillis: 1_700_000_090_000,
});
expect(
resourceCanvasResourceEditSubmissionBlocksDraft(
resourceCanvasResourceEditSubmissionBlocksResource(
failed,
'assets/source-art.png',
),
@@ -1,8 +1,9 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { repoPath } from './repoPath';
/**
* 「当前使用」资源卡的发光档案:声明级断言。
*
@@ -12,7 +13,7 @@ import { describe, expect, it } from 'vitest';
*/
function stylesSource() {
return readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
}
@@ -32,11 +32,11 @@ import {
React,
readFileSync,
render,
resolve,
screen,
vi,
waitFor,
} from './appSurface/harness';
import { repoPath } from './repoPath';
import {
declaration,
parseStyleSheet,
@@ -294,10 +294,7 @@ function cardFor(label: string) {
*/
function visualDeclarationsForCard(card: HTMLElement) {
const rules = parseStyleSheet(
readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
'utf8',
),
readFileSync(repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8'),
);
const kind = card.getAttribute('data-preview-kind');
const hasAlpha = card.getAttribute('data-preview-has-alpha');
@@ -381,7 +378,7 @@ describe('资源卡棋盘格底按真实 alpha 决定', () => {
it('透明图自身不被填底色:没有任何规则给 .game-resource-card-visual > img 声明背景', () => {
const rules = parseStyleSheet(
readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
),
);
@@ -409,7 +406,7 @@ describe('资源卡棋盘格底按真实 alpha 决定', () => {
// 这条用例必须同步改成「有判据才铺」。
const rules = parseStyleSheet(
readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
),
);
@@ -1,6 +1,5 @@
// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
cleanup,
@@ -15,6 +14,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import type { GameCreationAppAssetManifestEntry } from '../../../packages/shared/src/contracts/gameCreationApp';
import { ResourceClassificationPanel } from '../src/view/project-development/ResourceClassificationPanel';
import { repoPath } from './repoPath';
import {
declaration,
parseStyleSheet,
@@ -431,8 +431,7 @@ describe('ResourceClassificationPanel 编辑素材标签', () => {
test('资源卡工具条入口 label 与面板标题一致,不再是「分类与标签」', () => {
const viewSource = readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/view/project-development/index.tsx',
),
'utf8',
@@ -452,8 +451,7 @@ describe('ResourceClassificationPanel 编辑素材标签', () => {
test('标签 pill 的删除按钮热区不小于 32px', () => {
const panelStyles = readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/features/project-workspace/resourceClassificationTagPanel.css',
),
'utf8',
@@ -606,7 +604,7 @@ describe('ResourceClassificationPanel 不再承载删除素材', () => {
*/
test('底部只有一个「添加」并靠右下角', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const rule = styles.match(
@@ -641,7 +639,7 @@ describe('ResourceClassificationPanel 不再承载删除素材', () => {
*/
describe('ResourceClassificationPanel 标签区可滚动', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const rules = parseStyleSheet(styles);
@@ -1,6 +1,5 @@
// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
cleanup,
@@ -14,6 +13,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import type { GameCreationAppAssetManifestEntry } from '../../../packages/shared/src/contracts/gameCreationApp';
import { ResourceTypePanel } from '../src/view/project-development/ResourceTypePanel';
import { repoPath } from './repoPath';
import {
declaration,
parseStyleSheet,
@@ -106,8 +106,7 @@ function optionsList() {
function panelStyleSheet() {
return parseStyleSheet(
readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css',
),
'utf8',
@@ -279,8 +278,7 @@ describe('ResourceTypePanel 设置素材类型', () => {
expect(declaration(idle, 'background')).toBe('rgb(255 255 255 / 62%)');
const source = readFileSync(
resolve(
process.cwd(),
repoPath(
'apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css',
),
'utf8',
@@ -1,10 +1,10 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import type { GameCreationAppPreviewState } from '../../../packages/shared/src/contracts/gameCreationApp';
import { resolveSessionPreviewOnProjectOpen } from '../src/features/app-shell/sessionPreview';
import { repoPath } from './repoPath';
const projectPath = '/tmp/session-preview-project';
@@ -172,14 +172,11 @@ describe('进入项目时的预览活体核验', () => {
describe('本地预览记录的生命周期(Rust 侧结构性守卫)', () => {
const mainSource = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src-tauri/src/main.rs'),
repoPath('apps/ai-game-creator-shell/src-tauri/src/main.rs'),
'utf8',
);
const previewSource = readFileSync(
resolve(
process.cwd(),
'apps/ai-game-creator-shell/src-tauri/src/preview.rs',
),
repoPath('apps/ai-game-creator-shell/src-tauri/src/preview.rs'),
'utf8',
);
@@ -1,11 +1,12 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { repoPath } from './repoPath';
type Rgba = readonly [number, number, number, number];
const themePath = resolve(process.cwd(), 'packages/shared/src/theme.css');
const themePath = repoPath('packages/shared/src/theme.css');
function getCssBlock(source: string, selector: string) {
const selectorIndex = source.indexOf(selector);