补「回到资源总览」命中区域的守卫断言

- 成因回填:分页态标题栏右端的「资源总览」(回到资源总览)按钮点不动,是常驻工具条带 `.game-resource-book-tools` 造成的——它是横跨整行、`z-index: 40` 的绝对定位层,正好盖在栏目标题栏那条带(`z-index: 30`)上,把按钮的命中区域整段吃掉。上一提交撤掉这条带后问题已消失,本次只补守卫,不改实现。
- project-development.suite.ts:新增声明级守卫,钉住浮层提示层(`.game-resource-book-notices`)与返回按钮所在标题栏的层级关系——提示层 `z-index` 高于标题栏(提示要看得见),因此容器必须 `pointer-events: none`、只有提示条自己 `pointer-events: auto`;提示条顶边 ≥ 标题栏 `min-height`,连视觉重叠都不发生;并反向断言常驻工具条带在 styles.css 与 index.tsx 里都已不存在(变异实测:把容器改回 `pointer-events: auto` 该用例立即红灯)。
- project-development.suite.ts:新增渲染级守卫——附件导入失败条在场(提示层挂载)时进入分页态,断言返回按钮不在提示层容器内、不是 disabled,点击后 `data-resource-book-view` 回到 `main` 且「资源总览」region 在场。
- 说明:jsdom 不做布局命中判定,像素级"是否被盖住"只能由声明级的层级/命中声明 + 真机复核确认;渲染级用例钉的是"提示层在场时返回入口仍然渲染、可点、并真的接在返回总览上"。
This commit is contained in:
2026-09-11 00:57:52 +08:00
parent 5e29b1bb57
commit 3bd4daf5d5
@@ -1605,6 +1605,135 @@ export function registerProjectWorkbenchFoundationTests() {
expect(searchTrigger().className).not.toContain('is-active');
});
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'),
'utf8',
);
const tsxSource = readFileSync(
resolve(
process.cwd(),
'apps/ai-game-creator-shell/src/view/project-development/index.tsx',
),
'utf8',
);
const sceneZIndex = styleNumber(
styleRuleBody(styles, '\\.game-resource-book-scene'),
'z-index',
);
const titlebar = styleRuleBody(
styles,
'\\.game-resource-book-scene-titlebar',
);
const titlebarZIndex = styleNumber(titlebar, 'z-index');
const titlebarHeight = styleNumber(titlebar, 'min-height');
expect(titlebarZIndex).toBeGreaterThan(sceneZIndex);
expect(titlebarHeight).toBeGreaterThan(0);
// 提示层为了看得见必须浮在栏目标题栏之上——正因为如此,它必须把命中区域限死在提示条
// 自己的盒子里:容器 `pointer-events: none` + 提示条 `auto`。分页态标题栏右端的
// 「资源总览」(回到资源总览)就在这条带里,容器一旦吃点击,那颗按钮就点不动。
const notices = styleRuleBody(styles, '\\.game-resource-book-notices');
expect(styleNumber(notices, 'z-index')).toBeGreaterThan(titlebarZIndex);
expect(notices).toMatch(/pointer-events:\s*none/u);
expect(
styleRuleBody(styles, '\\.game-resource-book-notices > \\*'),
).toMatch(/pointer-events:\s*auto/u);
// 提示条顶边在标题栏那条带以下:连视觉重叠都不会发生,不只是命中被放行。
expect(styleNumber(notices, 'top')).toBeGreaterThanOrEqual(titlebarHeight);
// 成因本身也钉住:横跨整行、z-index 40 的常驻工具条带(当初为常驻搜索框引入)已经不在了,
// 它盖在标题栏那条带上时,正好吃掉了返回按钮的命中区域。
expect(styles).not.toContain('game-resource-book-tools');
expect(tsxSource).not.toContain('game-resource-book-tools');
});
it('returns to the overview from the paged titlebar while the notice layer is on screen', async () => {
const manifest = createGameCreationAppManifest(
'workbench-return-overview-hit',
'回到总览命中区测试',
);
manifest.assets = [
{
id: 'return-overview-art',
kind: 'character',
mediaType: 'image/png',
localPath: 'assets/return-hero.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-return-overview-hit',
manifest,
attachments: [
{
fileName: 'broken.txt',
mediaType: 'text/plain',
status: 'failed' as const,
error: '附件导入失败',
},
],
recentRunStatus: null,
recentRunStopReason: null,
supervisor: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
// 提示层在场:附件导入失败条渲染在这层浮层里,而这层浮层不属于画本场景。
const notices = document.querySelector('.game-resource-book-notices');
expect(notices).not.toBeNull();
expect(notices?.querySelector('.game-attachment-errors')).not.toBeNull();
expect(notices?.closest('.game-resource-book-scene')).toBeNull();
expect(notices?.querySelector('.game-resource-book-scene')).toBeNull();
await openResourceBookCategory('角色与对象');
const activeTitlebar = document.querySelector<HTMLElement>(
'.game-resource-book-scene-titlebar.is-active',
);
expect(activeTitlebar).not.toBeNull();
const returnButton = within(activeTitlebar as HTMLElement).getByRole(
'button',
{ name: '收起资源' },
) as HTMLButtonElement;
// 按钮可点:不是 disabled,祖先链上也没有提示层那类跨整行的浮层容器。
expect(returnButton.disabled).toBe(false);
expect(returnButton.closest('.game-resource-book-notices')).toBeNull();
// jsdom 不做布局命中的判定,能钉住的是"层级关系 + 按钮真的接在返回总览上":
// 默认运行时不带 aria-disabled、点击真的切回总览态。
fireEvent.click(returnButton);
expect(
document
.querySelector('[data-resource-book-view]')
?.getAttribute('data-resource-book-view'),
).toBe('main');
expect(screen.getByRole('region', { name: '资源总览' })).not.toBeNull();
});
it('paints the marquee selection box with the scene token root and non-empty geometry', async () => {
const manifest = createGameCreationAppManifest(
'workbench-marquee-colour',