WIP: AGC 资源画布与替换改造 V3.0 #316

Draft
suzmii wants to merge 131 commits from feat/agc-canvas-resource-workbench-v3 into master
4 changed files with 87 additions and 73 deletions
Showing only changes of commit 691d0a8ce6 - Show all commits
@@ -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,
@@ -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 () => {
@@ -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<HTMLElement>('.game-resource-card');
const wideImageCard = getResourceDetailButton(
'image-0.png',
).closest<HTMLElement>('.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<HTMLElement>('.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');
@@ -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(<DerivedWorkbench />);
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('编辑提示词'), {