Merge remote-tracking branch 'origin/master' into opt/design_agent
This commit is contained in:
@@ -4633,7 +4633,12 @@ export default function ProjectDevelopmentView({
|
||||
passive: false,
|
||||
});
|
||||
return () => manager.removeEventListener('wheel', handleResourceBookWheel);
|
||||
}, [handleResourceBookWheel, mode]);
|
||||
}, [
|
||||
handleResourceBookWheel,
|
||||
mode,
|
||||
// UI 编辑器会卸载整个资源 manager;返回时 ref 指向新节点,必须重新绑定原生 wheel。
|
||||
uiEditorRoute,
|
||||
]);
|
||||
|
||||
const handleResourceBookMainPointerDown = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
|
||||
@@ -1240,6 +1240,32 @@ async function openMainProject(projectPath: string) {
|
||||
).not.toBeNull();
|
||||
}
|
||||
|
||||
export function installResizeObserverStub() {
|
||||
let observerCount = 0;
|
||||
let observerDisconnected = false;
|
||||
class TestResizeObserver {
|
||||
constructor(readonly callback: ResizeObserverCallback) {
|
||||
observerCount += 1;
|
||||
}
|
||||
|
||||
observe() {}
|
||||
|
||||
unobserve() {}
|
||||
|
||||
disconnect() {
|
||||
observerDisconnected = true;
|
||||
}
|
||||
}
|
||||
Object.defineProperty(window, 'ResizeObserver', {
|
||||
configurable: true,
|
||||
value: TestResizeObserver,
|
||||
});
|
||||
return {
|
||||
observerCount: () => observerCount,
|
||||
observerDisconnected: () => observerDisconnected,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetLlmModelCatalogCacheForTest();
|
||||
vi.spyOn(clientApi, 'loadClientLlmModels').mockResolvedValue({
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
||||
type GameCreationAgentRunTrace,
|
||||
getResourceSelectButton,
|
||||
installResizeObserverStub,
|
||||
it,
|
||||
mockRoleAgentReply,
|
||||
openResourceFilterPanel,
|
||||
@@ -4708,25 +4709,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
manuallyPlaced: false,
|
||||
},
|
||||
];
|
||||
let observerCount = 0;
|
||||
let observerDisconnected = false;
|
||||
class TestResizeObserver {
|
||||
constructor(readonly callback: ResizeObserverCallback) {
|
||||
observerCount += 1;
|
||||
}
|
||||
|
||||
observe() {}
|
||||
|
||||
unobserve() {}
|
||||
|
||||
disconnect() {
|
||||
observerDisconnected = true;
|
||||
}
|
||||
}
|
||||
Object.defineProperty(window, 'ResizeObserver', {
|
||||
configurable: true,
|
||||
value: TestResizeObserver,
|
||||
});
|
||||
const resizeObserver = installResizeObserverStub();
|
||||
const animationFrame = vi.spyOn(window, 'requestAnimationFrame');
|
||||
const originalGetBoundingClientRect =
|
||||
HTMLElement.prototype.getBoundingClientRect;
|
||||
@@ -4818,7 +4801,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
/^url\(#.+-asset-reference-arrow\)$/u,
|
||||
);
|
||||
});
|
||||
expect(observerCount).toBe(1);
|
||||
expect(resizeObserver.observerCount()).toBe(1);
|
||||
expect(
|
||||
overlay
|
||||
.querySelector('[data-testid="resource-dependency-overlay-scene"]')
|
||||
@@ -4865,7 +4848,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
),
|
||||
);
|
||||
rendered.unmount();
|
||||
expect(observerDisconnected).toBe(true);
|
||||
expect(resizeObserver.observerDisconnected()).toBe(true);
|
||||
expect(removeViewportListener).toHaveBeenCalledWith(
|
||||
'scroll',
|
||||
expect.any(Function),
|
||||
@@ -5916,6 +5899,144 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
);
|
||||
});
|
||||
|
||||
it('restores resource canvas panning after returning from the UI editor', async () => {
|
||||
installResizeObserverStub();
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-ui-editor-return-pan',
|
||||
'UI 编辑器返回平移测试',
|
||||
);
|
||||
manifest.assets = [
|
||||
{
|
||||
id: 'ui-design-resource',
|
||||
kind: 'UI',
|
||||
mediaType: 'application/json',
|
||||
localPath: 'assets/ui-design.json',
|
||||
source: { kind: 'generated' },
|
||||
},
|
||||
];
|
||||
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: manifest.projectId,
|
||||
mode: args?.mode,
|
||||
revision: 0,
|
||||
positions: [],
|
||||
updatedAt: 0,
|
||||
};
|
||||
}
|
||||
if (command === 'update_local_project_resource_canvas_layout') {
|
||||
return {
|
||||
status: 'updated',
|
||||
layout: {
|
||||
schemaVersion: 'game-creator-resource-layout.v1',
|
||||
projectId: manifest.projectId,
|
||||
mode: args?.mode,
|
||||
revision: 1,
|
||||
positions: args?.positions,
|
||||
updatedAt: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (command === 'load_ui_design_state') {
|
||||
return {
|
||||
revision: 0,
|
||||
state: {
|
||||
ui_trees: [],
|
||||
ui_design_images: {},
|
||||
sprite_assets: {},
|
||||
font_assets: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
render(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
projectName: manifest.name,
|
||||
projectPath: '/tmp/workbench-ui-editor-return-pan',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: null,
|
||||
recentRunStopReason: null,
|
||||
supervisor: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
await openResourceBookCategory('UI 交互');
|
||||
const canvas = await screen.findByRole('region', { name: 'UI 交互' });
|
||||
const scene = document.querySelector<HTMLElement>(
|
||||
'[data-resource-book-view="child"] .game-resource-book-scene',
|
||||
);
|
||||
|
||||
const wheel = (target: HTMLElement, deltaY: number) => {
|
||||
const event = new WheelEvent('wheel', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
deltaY,
|
||||
clientX: 120,
|
||||
clientY: 100,
|
||||
});
|
||||
act(() => target.dispatchEvent(event));
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
};
|
||||
wheel(scene ?? canvas, 120);
|
||||
const world = canvas.querySelector<HTMLElement>('[data-resource-viewport]');
|
||||
expect(world?.getAttribute('data-resource-viewport')).toMatch(
|
||||
/^(?!48,48,1)/u,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: '选中资源:UI 交互 ui-design.json',
|
||||
}),
|
||||
);
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'UI 编辑器' }));
|
||||
await screen.findByRole('button', { name: '返回资源' });
|
||||
fireEvent.click(screen.getByRole('button', { name: '返回资源' }));
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('button', { name: '返回资源' })).toBeNull(),
|
||||
);
|
||||
|
||||
const restoredCanvas = await screen.findByRole('region', {
|
||||
name: 'UI 交互',
|
||||
});
|
||||
const restoredWorld = restoredCanvas.querySelector<HTMLElement>(
|
||||
'[data-resource-viewport]',
|
||||
);
|
||||
const beforeReturn = world?.getAttribute('data-resource-viewport');
|
||||
const restoredBeforePan = restoredWorld?.getAttribute(
|
||||
'data-resource-viewport',
|
||||
);
|
||||
expect(restoredBeforePan).toBe(beforeReturn);
|
||||
|
||||
const restoredScene = document.querySelector<HTMLElement>(
|
||||
'[data-resource-book-view="child"] .game-resource-book-scene',
|
||||
);
|
||||
const restoredWheelEvent = new WheelEvent('wheel', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
deltaY: 120,
|
||||
clientX: 120,
|
||||
clientY: 100,
|
||||
});
|
||||
act(() =>
|
||||
(restoredScene ?? restoredCanvas).dispatchEvent(restoredWheelEvent),
|
||||
);
|
||||
expect(restoredWheelEvent.defaultPrevented).toBe(true);
|
||||
expect(restoredWorld?.getAttribute('data-resource-viewport')).not.toBe(
|
||||
restoredBeforePan,
|
||||
);
|
||||
});
|
||||
|
||||
it('clears the resource preview cache and cancels the old preview scope on project switch', async () => {
|
||||
// 60d8b8fbb 删掉了从未被写入的 `resourcePreviewVersionByResourceId` 死接线,原先钉
|
||||
// 那条 prop 与它的清空语句的断言随之取消;用例真正要守的意图不变——项目切换必须把
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 踩坑与排障记录
|
||||
|
||||
## 2026-09-14 UI 编辑器返回后资源画布滚轮平移失效
|
||||
|
||||
- **现象**:资源管理打开 UI 编辑器再返回后,资源画布滚轮平移/缩放不再响应;返回前同一手势正常。
|
||||
- **原因**:资源画布的非 passive `wheel` 监听绑定在 `resourceBookManagerRef` 当前 DOM 上,但 effect 只依赖 `handleResourceBookWheel` 与 `mode`。UI 编辑器切换会卸载旧 manager 并挂载新 manager,依赖不变导致新节点没有重新绑定监听。
|
||||
- **处理**:将 `uiEditorRoute` 纳入 wheel effect 依赖,使进入/退出 UI 编辑器时先清理旧节点监听,再给返回后的新 manager 绑定同一处理器。
|
||||
- **验证**:`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts -t "restores resource canvas panning"`;回归用例覆盖打开栏目、wheel 平移、进入 UI 编辑器、返回并再次 wheel 平移。
|
||||
- **关联**:`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`。
|
||||
|
||||
## 2026-09-14 AGC 壳 Rust 套件按「一片一 job」拆分,且分片必须自校验覆盖
|
||||
|
||||
- **现象**:`AI game creator shell Rust tests` 一直是客户端 CI 的关键路径。run 2097 实测 15 分 27 秒,其中 `apps/ai-game-creator-shell/src-tauri` 的 bin target 单测(2466 条)一条 `cargo test -- --test-threads=1` 串行占 507 秒。
|
||||
|
||||
Reference in New Issue
Block a user