修复 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:
@@ -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 />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user