按 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:
@@ -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;
|
||||
|
||||
+17
-15
@@ -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(...)` 没有 fallback:token 缺失或被写透明,框选就没有颜色。
|
||||
@@ -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);
|
||||
}
|
||||
+2
-3
@@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
+176
-74
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-3
@@ -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',
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user