From 7e6f7555eb391ef98cc64c8061ea012f6f75615e Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Thu, 10 Sep 2026 19:59:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AGC=20=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=A1=86=E8=A2=AB=20display:=20none=20?= =?UTF-8?q?=E8=AF=AF=E5=85=B3=E5=AF=BC=E8=87=B4=E7=9A=84=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E6=AD=BB=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - styles.css:`.game-resource-search` 的 `display: none` 恢复成 `display: flex`(原始提交 3a8dff140 里就是 flex,PR #193 重排工作台时被改成 none),并补 `position: relative; z-index: 40`、常态 `align-self: center`、与右下角缩放 Dock 同款的浮层阴影。 - styles.css:`.game-resource-live-notice` 同样补 `position: relative; z-index: 40`;「清除搜索并定位」按钮只在这条提示条里,提示条被画本场景盖住时搜索链路依旧是断的。 - 根因(堆叠顺序):资源卡与栏目标题栏都画在 `.game-resource-book-scene`(absolute + z-index 20,子画布态还有一层不透明点阵底)里,而搜索框与提示条是 `.game-resource-book-manager` 的流式子节点;定位元素按绘制顺序整体盖在流内内容之上,所以只放开 display 等于"画了但看不见、点不到"。 - 可见性口径:AGC 的 vitest 没开 `css: true`、不加载 styles.css,`toBeVisible()` 在这里是恒真的假守卫;新增的守卫按本仓既有口径读 CSS 源文件做声明级断言(不是 display:none、z-index 高于画本场景)。 - 行为用例:总览态输入即过滤总览摞上的卡片;分页画布态同一搜索条件继续生效、清空后卡片恢复,证明这条链路确实由搜索框驱动。 - 端到端用例:搜索条件挡掉刚提交的新素材时保留条件并提示,点「清除搜索并定位」后清空搜索并定位选中新素材。 --- apps/ai-game-creator-shell/src/styles.css | 16 ++- .../appSurface/project-development.suite.ts | 124 ++++++++++++++++++ .../projectResourceLiveIntegration.test.tsx | 40 ++++++ 3 files changed, 179 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 96a2aa988..78d961934 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -6157,15 +6157,26 @@ iframe.preview-frame { } } +/* + * 资源画本的卡片与栏目标题栏都画在铺满资源区的 `.game-resource-book-scene` + * (z-index 20)里,流式工具条默认被它整个盖住。搜索框和它下面的提示条必须抬到 + * 场景之上(与右下角缩放 Dock 同为 40),否则放开 display 也只是"画了但看不见"。 + */ .game-resource-search { - display: none; + position: relative; + z-index: 40; + display: flex; + /* 常态居中,避开总览态标题和分页态栏目标题栏左右两端的内容。 */ + align-self: center; align-items: center; gap: 8px; + width: min(300px, 100%); margin: 10px 12px 0; padding: 0 10px; border: 1px solid #ecdcd4; border-radius: 10px; background: #fff; + box-shadow: 0 6px 18px rgb(112 70 52 / 12%); color: #a08073; } @@ -6257,6 +6268,9 @@ iframe.preview-frame { } .game-resource-live-notice { + /* 「清除搜索并定位」只在搜索把刚提交的资源挡掉时出现,提示条同样要在画本场景之上。 */ + position: relative; + z-index: 40; display: flex; align-items: center; justify-content: space-between; diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 7927779a8..3a53a01c7 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -191,6 +191,17 @@ function createDeferred() { return { promise, reject, resolve }; } +/** 取 CSS 源文件里某条规则的声明体;jsdom 不加载这些样式表,可见性只能钉在声明上。 */ +function styleRuleBody(styles: string, selector: string) { + const match = new RegExp(`${selector}\\s*\\{([^}]*)\\}`, 'su').exec(styles); + expect(match, `${selector} 规则缺失`).not.toBeNull(); + return match![1]!; +} + +function styleNumber(body: string, property: string) { + return Number(new RegExp(`${property}\\s*:\\s*(\\d+)`).exec(body)?.[1]); +} + export function registerProjectWorkbenchFoundationTests() { it('renders the first project workbench slice with honest disabled run and local approval UI', () => { const manifest = createGameCreationAppManifest( @@ -1117,6 +1128,119 @@ export function registerProjectWorkbenchFoundationTests() { ); }); + it('keeps the resource search box above the resource book scene in both views', () => { + const styles = readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + 'utf8', + ); + // 卡片(z-index 25)与栏目标题栏(30)都画在铺满资源区的场景里:工具条必须比场景更高, + // 否则放开 display 也只是"画了但看不见、点不到"。 + const sceneZIndex = styleNumber( + styleRuleBody(styles, '\\.game-resource-book-scene'), + 'z-index', + ); + expect(sceneZIndex).toBeGreaterThan(0); + + const search = styleRuleBody(styles, '\\.game-resource-search'); + expect(search).not.toMatch(/display:\s*none/u); + expect(search).toMatch(/display:\s*flex/u); + expect(search).toMatch(/position:\s*relative/u); + expect(styleNumber(search, 'z-index')).toBeGreaterThan(sceneZIndex); + + // 「清除搜索并定位」只在搜索把刚提交的资源挡掉时出现,它所在的提示条同样要在场景之上。 + const notice = styleRuleBody(styles, '\\.game-resource-live-notice'); + expect(notice).toMatch(/position:\s*relative/u); + expect(styleNumber(notice, 'z-index')).toBeGreaterThan(sceneZIndex); + }); + + it('filters resources through the resource search box in both views', async () => { + const manifest = createGameCreationAppManifest( + 'workbench-search-visibility', + '资源搜索可见性测试', + ); + manifest.assets = [ + { + id: 'search-alpha', + kind: 'character', + mediaType: 'image/png', + localPath: 'assets/alpha-hero.png', + source: { kind: 'generated', taskId: 'art-asset-plan' }, + }, + { + id: 'search-beta', + kind: 'character', + mediaType: 'image/png', + localPath: 'assets/beta-enemy.png', + source: { kind: 'generated', taskId: 'art-asset-plan' }, + }, + ]; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'read_local_project_resource_graph') { + return resourceGraphForInputs(args); + } + if (command === 'read_local_project_resource_canvas_layout') { + return { + schemaVersion: 'game-creator-resource-layout.v1', + projectId: args?.expectedProjectId, + mode: args?.mode, + revision: 0, + positions: [], + updatedAt: 0, + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + + render( + React.createElement(ProjectDevelopmentView, { + projectName: manifest.name, + projectPath: '/tmp/workbench-search-visibility', + manifest, + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + supervisor: React.createElement('div', null, '项目总控'), + onHomeOpen: vi.fn(), + onProjectsOpen: vi.fn(), + }), + ); + + // 搜索框是常驻的真输入框(图标 + input 都在同一个 label 里),不是悬停才展开的浮层。 + const searchInput = () => + screen.getByLabelText('搜索项目资源') as HTMLInputElement; + expect(searchInput().type).toBe('search'); + expect(searchInput().closest('label')?.className).toContain( + 'game-resource-search', + ); + + const overviewCards = () => + document.querySelectorAll( + '.game-resource-book-scene-card[data-resource-book-category="art"]', + ); + await waitFor(() => expect(overviewCards()).toHaveLength(2)); + + // 总览态:输入即过滤总览摞上的卡片。 + fireEvent.change(searchInput(), { target: { value: 'alpha-hero' } }); + await waitFor(() => expect(overviewCards()).toHaveLength(1)); + + // 分页画布态:同一条件继续生效,输入框仍可读写。 + await openResourceBookCategory('美术资源'); + expect(searchInput().value).toBe('alpha-hero'); + await waitFor(() => + expect(getResourceSelectButton('alpha-hero.png')).not.toBeNull(), + ); + expect(queryResourceSelectButton('beta-enemy.png')).toBeNull(); + + // 清空后恢复:证明这条链路确实由搜索框驱动。 + fireEvent.change(searchInput(), { target: { value: '' } }); + await waitFor(() => + expect(queryResourceSelectButton('beta-enemy.png')).not.toBeNull(), + ); + }); + it('marks newly added resources on inactive section tabs until the user opens them', async () => { const manifest = createGameCreationAppManifest( 'workbench-resource-unread', diff --git a/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx b/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx index 4788d1942..597ea9ee2 100644 --- a/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx +++ b/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx @@ -562,6 +562,46 @@ describe('project resource live canvas integration', () => { ).toBe('true'); }); + it('keeps the search condition, offers an explicit clear-and-locate, then locates the new asset', async () => { + const { deriveCalls } = installTauri(); + render(); + + fireEvent.click(screen.getByRole('button', { name: '打开美术资源' })); + fireEvent.click(await findResourceSelectButton('source-art.png')); + const toolbar = await screen.findByRole('toolbar', { + name: '图片工具栏', + }); + fireEvent.click(within(toolbar).getByRole('button', { name: '快速编辑' })); + const panel = await screen.findByRole('dialog', { + name: '快速编辑图片', + }); + await setComposerText( + within(panel).getByLabelText('快速编辑提示词'), + '把角色头发设定改为红色', + ); + // 搜索条件在提交前就存在,新素材(文档分类)不会命中它:走"被当前搜索隐藏"分支。 + const search = screen.getByLabelText('搜索项目资源') as HTMLInputElement; + fireEvent.change(search, { target: { value: 'source-art' } }); + fireEvent.click(within(panel).getByRole('button', { name: '修改' })); + await waitFor(() => expect(deriveCalls).toHaveLength(1)); + + // 保留条件并明确提示,只通过显式动作清除条件并定位,不静默改搜索。 + expect( + await screen.findByText('新资源已保存,但被当前搜索条件隐藏'), + ).not.toBeNull(); + expect(search.value).toBe('source-art'); + fireEvent.click(screen.getByRole('button', { name: '清除搜索并定位' })); + + expect(search.value).toBe(''); + const operationId = String(deriveCalls[0]?.operationId); + expect( + (await findResourceSelectButton(`${operationId}-rules.md`)).getAttribute( + 'aria-pressed', + ), + ).toBe('true'); + expect(screen.queryByText('新资源已保存,但被当前搜索条件隐藏')).toBeNull(); + }); + it('hides the canvas generation entry when the client bridge is unavailable', async () => { render();