diff --git a/apps/ai-game-creator-shell/tests/appSurface/harness.ts b/apps/ai-game-creator-shell/tests/appSurface/harness.ts index 01f3dd0c0..d42312188 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/harness.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/harness.ts @@ -199,6 +199,38 @@ function submitChat(value: string) { fireEvent.click(screen.getByRole('button', { name: '发送' })); } +// 同一张资源卡上有两个按钮:打开详情与 V3 新增的 @ 引用按钮,二者的可访问名都包含 +// 资源文件名,因此按文件名正则查询会同时命中两个元素。资源详情按钮的可访问名模板固定为 +// `打开资源详情:<分类标签> <资源文件名>`(分类标签内不含空格),这里按模板做精确匹配。 +function escapeRegExpLiteral(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function resourceDetailButtonName(label: string) { + return new RegExp(`^打开资源详情:\\S+ ${escapeRegExpLiteral(label)}$`); +} + +function getResourceDetailButton(label: string) { + return screen.getByRole('button', { name: resourceDetailButtonName(label) }); +} + +function findResourceDetailButton( + label: string, + options?: { timeout?: number; interval?: number }, +) { + return screen.findByRole( + 'button', + { name: resourceDetailButtonName(label) }, + options, + ); +} + +function queryResourceDetailButton(label: string) { + return screen.queryByRole('button', { + name: resourceDetailButtonName(label), + }); +} + function emptyProjectPolicy() { return { path: '.agent/policy.json', @@ -1094,8 +1126,10 @@ export { describe, emptyProjectPolicy, expect, + findResourceDetailButton, fireEvent, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, + getResourceDetailButton, it, mockRoleAgentReply, nativeClipboardMock, @@ -1103,6 +1137,7 @@ export { pickProjectFromLauncher, ProjectDevelopmentView, projectSupervisorResponseStream, + queryResourceDetailButton, React, readFileSync, render, diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index fdf120ea6..83cb9242a 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -9,10 +9,13 @@ import { createGameCreationAppManifest, createProjectSupervisorRuntimeHarness, expect, + findResourceDetailButton, fireEvent, + getResourceDetailButton, it, nativeClipboardMock, pickProjectFromLauncher, + queryResourceDetailButton, React, render, renderAppAt, @@ -312,14 +315,12 @@ export function registerClientHomeTests() { pickProjectFromLauncher(projectPath); const runButton = await screen.findByRole('tab', { name: '运行' }); expect(runButton.getAttribute('data-unavailable')).toBe('true'); - expect(screen.queryByRole('button', { name: /live-hero\.png/ })).toBeNull(); + expect(queryResourceDetailButton('live-hero.png')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '同步最新 manifest' })); await openResourceBookCategory('美术资源'); - expect( - await screen.findByRole('button', { name: /live-hero\.png/ }), - ).not.toBeNull(); + expect(await findResourceDetailButton('live-hero.png')).not.toBeNull(); await openResourceBookCategory('项目版本'); expect( await screen.findByRole('button', { name: /版本 1/ }), @@ -460,9 +461,7 @@ export function registerClientHomeTests() { pickProjectFromLauncher(projectPath); const runButton = await screen.findByRole('tab', { name: '运行' }); expect(runButton.getAttribute('data-unavailable')).toBe('true'); - expect( - screen.queryByRole('button', { name: /runtime-live-hero\.png/ }), - ).toBeNull(); + expect(queryResourceDetailButton('runtime-live-hero.png')).toBeNull(); await waitFor(() => { expect(runtimeHarness.listen).toHaveBeenCalledWith( 'game-creator-manifest-invalidated', @@ -488,11 +487,9 @@ export function registerClientHomeTests() { await openResourceBookCategory('美术资源'); expect( - await screen.findByRole( - 'button', - { name: /runtime-live-hero\.png/ }, - { timeout: 5_000 }, - ), + await findResourceDetailButton('runtime-live-hero.png', { + timeout: 5_000, + }), ).not.toBeNull(); await openResourceBookCategory('项目版本'); expect( @@ -665,18 +662,14 @@ export function registerClientHomeTests() { fireEvent.click(screen.getByRole('button', { name: '项目组' })); pickProjectFromLauncher(secondProjectPath); await openResourceBookCategory('美术资源'); - expect( - await screen.findByRole('button', { name: /second\.png/ }), - ).not.toBeNull(); + expect(await findResourceDetailButton('second.png')).not.toBeNull(); await act(async () => { resolveStaleRefresh(staleFirstManifest); await staleRefresh; }); - expect( - screen.queryByRole('button', { name: /stale-first\.png/ }), - ).toBeNull(); - expect(screen.getByRole('button', { name: /second\.png/ })).not.toBeNull(); + expect(queryResourceDetailButton('stale-first.png')).toBeNull(); + expect(getResourceDetailButton('second.png')).not.toBeNull(); }); it('starts from the client home and opens a project in the same window', async () => { 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 1323d0e6d..806c64f5b 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 @@ -22,14 +22,17 @@ import { createProjectSupervisorRuntimeHarness, emptyProjectPolicy, expect, + findResourceDetailButton, fireEvent, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, type GameCreationAgentRunTrace, + getResourceDetailButton, it, mockRoleAgentReply, pickProjectFromLauncher, ProjectDevelopmentView, projectSupervisorResponseStream, + queryResourceDetailButton, React, readFileSync, render, @@ -710,7 +713,7 @@ export function registerProjectWorkbenchFoundationTests() { ), ).toEqual(['设计文档', '美术资源', '音乐音效', '项目版本']); expect(graphResourceIds).toContain('asset:section-code'); - expect(screen.queryByRole('button', { name: /section\.js/ })).toBeNull(); + expect(queryResourceDetailButton('section.js')).toBeNull(); expect(screen.queryByRole('region', { name: '游戏代码' })).toBeNull(); expect(outline.querySelectorAll('small')).toHaveLength(0); expect(screen.getByRole('region', { name: '设计文档' })).not.toBeNull(); @@ -1282,7 +1285,7 @@ export function registerProjectWorkbenchFoundationTests() { .getByLabelText(/资源(?:依赖|类型)视图/) .classList.contains('game-resource-canvas--paged'), ).toBe(false); - expect(screen.queryByRole('button', { name: /game\.js/ })).toBeNull(); + expect(queryResourceDetailButton('game.js')).toBeNull(); expect(screen.queryByRole('region', { name: '游戏代码' })).toBeNull(); }); }); @@ -1604,9 +1607,7 @@ export function registerProjectWorkbenchFoundationTests() { const showResourcePage = (label: string) => openResourceBookCategory(label); await showResourcePage('美术资源'); - let heroDetailButton = await screen.findByRole('button', { - name: /打开资源详情:美术资源 hero\.png/, - }); + let heroDetailButton = await findResourceDetailButton('hero.png'); const heroCard = heroDetailButton.closest('.game-resource-card'); expect(heroCard?.textContent).not.toContain('hero.png'); expect(heroCard?.textContent).not.toContain('assets/hero.png'); @@ -1724,9 +1725,7 @@ export function registerProjectWorkbenchFoundationTests() { expect(screen.queryByRole('dialog', { name: 'theme.mp3' })).toBeNull(); await showResourcePage('美术资源'); - heroDetailButton = await screen.findByRole('button', { - name: /打开资源详情:美术资源 hero\.png/, - }); + heroDetailButton = await findResourceDetailButton('hero.png'); fireEvent.click(heroDetailButton); const heroFocus = await screen.findByRole('dialog', { name: 'hero.png' }); expect(within(heroFocus).getByText('assets/hero.png')).not.toBeNull(); @@ -1769,11 +1768,11 @@ export function registerProjectWorkbenchFoundationTests() { const search = screen.getByLabelText('搜索项目资源'); fireEvent.change(search, { target: { value: 'assets/hero.png' } }); await waitFor(() => expect(pause.mock.instances).toContain(typeVideo)); - expect(screen.getByRole('button', { name: /hero\.png/ })).not.toBeNull(); - expect(screen.queryByRole('button', { name: /intro\.mp4/ })).toBeNull(); - const typeHeroCard = screen - .getByRole('button', { name: /hero\.png/ }) - .closest('.game-resource-card'); + expect(getResourceDetailButton('hero.png')).not.toBeNull(); + expect(queryResourceDetailButton('intro.mp4')).toBeNull(); + const typeHeroCard = getResourceDetailButton('hero.png').closest( + '.game-resource-card', + ); await waitFor(() => expect( typeHeroCard?.querySelector('.game-resource-card-visual > img'), @@ -1899,9 +1898,9 @@ export function registerProjectWorkbenchFoundationTests() { document.querySelectorAll('.game-resource-card-visual > img'), ).toHaveLength(48); }); - const wideImageCard = screen - .getByRole('button', { name: /image-0.png/ }) - .closest('.game-resource-card'); + const wideImageCard = getResourceDetailButton( + 'image-0.png', + ).closest('.game-resource-card'); await waitFor(() => { expect( wideImageCard?.style.getPropertyValue('--resource-card-width'), @@ -1935,7 +1934,7 @@ export function registerProjectWorkbenchFoundationTests() { ); await openResourceBookCategory('美术资源'); await waitFor(() => - expect(screen.getByRole('button', { name: /next\.png/ })).not.toBeNull(), + expect(getResourceDetailButton('next.png')).not.toBeNull(), ); await waitFor(() => expect(observer.observedCount()).toBe(1)); act(() => observer.triggerVisible()); @@ -2059,18 +2058,18 @@ export function registerProjectWorkbenchFoundationTests() { name: /美术资源/, }), ); - const playerCard = screen - .getByRole('button', { name: /player\.png/ }) - .closest('.game-resource-card'); + const playerCard = getResourceDetailButton('player.png').closest( + '.game-resource-card', + ); expect(playerCard?.classList.contains('is-relation-version-binding')).toBe( true, ); fireEvent.click(screen.getByRole('button', { name: '按依赖' })); await waitFor(() => { - const dependencyPlayerCard = screen - .getByRole('button', { name: /player\.png/ }) - .closest('.game-resource-card'); + const dependencyPlayerCard = getResourceDetailButton( + 'player.png', + ).closest('.game-resource-card'); expect( dependencyPlayerCard?.classList.contains('is-relation-version-binding'), ).toBe(true); @@ -2427,7 +2426,7 @@ export function registerProjectWorkbenchFoundationTests() { ); await openResourceBookCategory('设计文档'); - fireEvent.click(await screen.findByRole('button', { name: /design\.md/ })); + fireEvent.click(await findResourceDetailButton('design.md')); const documentFocus = await screen.findByRole('dialog', { name: 'design.md', }); @@ -2439,7 +2438,7 @@ export function registerProjectWorkbenchFoundationTests() { expect(within(documentFocus).getByText('图片:远程图片')).not.toBeNull(); expect(documentFocus.hasAttribute('aria-modal')).toBe(false); await openResourceBookCategory('美术资源'); - fireEvent.click(screen.getByRole('button', { name: /icon\.svg/ })); + fireEvent.click(getResourceDetailButton('icon.svg')); expect(screen.queryByRole('dialog', { name: 'design.md' })).toBeNull(); const artDetails = await screen.findByRole('dialog', { name: 'icon.svg' }); expect(within(artDetails).getByText('assets/icon.svg')).not.toBeNull(); @@ -2504,7 +2503,7 @@ export function registerProjectWorkbenchFoundationTests() { ); await openResourceBookCategory('设计文档'); - fireEvent.click(screen.getByRole('button', { name: /blocked\.md/ })); + fireEvent.click(getResourceDetailButton('blocked.md')); expect((await screen.findByRole('alert')).textContent).toBe( '当前项目策略要求先确认读取文档,确认后请关闭详情并重试', ); @@ -2585,11 +2584,7 @@ export function registerProjectWorkbenchFoundationTests() { ); await openResourceBookCategory('音乐音效'); - fireEvent.click( - await screen.findByRole('button', { - name: '打开资源详情:音乐音效 focus.mp3', - }), - ); + fireEvent.click(await findResourceDetailButton('focus.mp3')); const audioDetails = await screen.findByRole('dialog', { name: 'focus.mp3', }); @@ -2904,12 +2899,8 @@ export function registerProjectWorkbenchFoundationTests() { ); fireEvent.change(search, { target: { value: '' } }); - let sourceCard = screen.getByRole('button', { - name: /spec-source\.json/, - }); - const targetCard = screen.getByRole('button', { - name: /ui-dependency\.json/, - }); + let sourceCard = getResourceDetailButton('spec-source.json'); + const targetCard = getResourceDetailButton('ui-dependency.json'); const referenceSelector = '[data-edge-kind="asset-reference"]' + '[data-source-resource-id="asset:dependency-spec"]' + @@ -2984,7 +2975,7 @@ export function registerProjectWorkbenchFoundationTests() { within(targetFocus).getByRole('button', { name: '收起资源' }), ); overlay = await screen.findByTestId('resource-dependency-overlay-document'); - sourceCard = screen.getByRole('button', { name: /spec-source\.json/ }); + sourceCard = getResourceDetailButton('spec-source.json'); await waitFor(() => expect(overlay.querySelector(referenceSelector)).not.toBeNull(), ); @@ -3730,11 +3721,9 @@ export function registerProjectWorkbenchFoundationTests() { await waitFor(() => expect(layoutReads).toBe(1)); await openResourceBookCategory('设计文档'); const cards = [0, 1, 2].map((depth) => - screen - .getByRole('button', { - name: new RegExp(`truncated-depth-${depth}\\.json`, 'u'), - }) - .closest('.game-resource-card'), + getResourceDetailButton(`truncated-depth-${depth}.json`).closest( + '.game-resource-card', + ), ); await waitFor(() => { expect(cards[0]?.getAttribute('style')).toContain('--resource-x: 0px'); @@ -3962,9 +3951,7 @@ export function registerProjectWorkbenchFoundationTests() { ); await openResourceBookCategory('美术资源'); - const cardButton = await screen.findByRole('button', { - name: '打开资源详情:美术资源 clickable.png', - }); + const cardButton = await findResourceDetailButton('clickable.png'); fireEvent.pointerDown(cardButton, { pointerId: 7, button: 0, @@ -4049,9 +4036,7 @@ export function registerProjectWorkbenchFoundationTests() { ); await openResourceBookCategory('美术资源'); - const cardButton = await screen.findByRole('button', { - name: '打开资源详情:美术资源 cancelable.png', - }); + const cardButton = await findResourceDetailButton('cancelable.png'); const card = cardButton.closest('.game-resource-card'); expect(card).not.toBeNull(); Object.defineProperties(card!, { @@ -4534,7 +4519,7 @@ export function registerProjectWorkbenchFoundationTests() { fireEvent.change(screen.getByLabelText('搜索项目资源'), { target: { value: 'hero.png' }, }); - fireEvent.click(await screen.findByRole('button', { name: /hero\.png/ })); + fireEvent.click(await findResourceDetailButton('hero.png')); const heroDetails = screen.getByRole('dialog', { name: 'hero.png' }); expect(within(heroDetails).getByText('assets/hero.png')).not.toBeNull(); expect(within(heroDetails).getByText('image/png')).not.toBeNull(); @@ -4587,9 +4572,9 @@ export function registerProjectWorkbenchFoundationTests() { ); await openResourceBookCategory('美术资源'); - const candidate = await screen.findByRole('button', { - name: /ui-prototype\.png(待视觉验收)/, - }); + const candidate = await findResourceDetailButton( + 'ui-prototype.png(待视觉验收)', + ); const candidateCard = candidate.closest('.game-resource-card'); expect(candidateCard?.textContent).not.toContain('画板 · 候选界面图'); expect(candidateCard?.textContent).not.toContain('assets/ui-prototype.png'); diff --git a/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx b/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx index 5c7c00307..5293b7e64 100644 --- a/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx +++ b/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx @@ -13,6 +13,7 @@ import ProjectDevelopmentView from '../src/view/project-development'; import { act, createGameCreationAppManifest, + findResourceDetailButton, fireEvent, render, screen, @@ -476,7 +477,7 @@ describe('project resource live canvas integration', () => { const { deriveCalls } = installTauri({ failFirstDerive: true }); render(); - fireEvent.click(await screen.findByRole('button', { name: /rules\.md/ })); + fireEvent.click(await findResourceDetailButton('rules.md')); fireEvent.click(screen.getByRole('button', { name: '编辑资源' })); expect(await screen.findByText('编辑现有资源')).not.toBeNull(); fireEvent.change(screen.getByLabelText('编辑提示词'), {