修复 AGC 资源搜索框被 display: none 误关导致的搜索链路死代码

- 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 高于画本场景)。
- 行为用例:总览态输入即过滤总览摞上的卡片;分页画布态同一搜索条件继续生效、清空后卡片恢复,证明这条链路确实由搜索框驱动。
- 端到端用例:搜索条件挡掉刚提交的新素材时保留条件并提示,点「清除搜索并定位」后清空搜索并定位选中新素材。
This commit is contained in:
2026-09-10 19:59:52 +08:00
parent 6f09bf3a00
commit 7e6f7555eb
3 changed files with 179 additions and 1 deletions
+15 -1
View File
@@ -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;
@@ -191,6 +191,17 @@ function createDeferred<T>() {
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<string, unknown>) => {
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',
@@ -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(<DerivedWorkbench includeArt />);
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(<DerivedWorkbench />);