// @vitest-environment jsdom import { act, cleanup, renderHook, waitFor } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { ProjectResourceCanvasLayout, ProjectResourceCanvasLayoutMode, ProjectResourceCanvasPosition, } from '../../../packages/shared/src/contracts/gameCreationApp'; import { GAME_CREATION_RESOURCE_LAYOUT_MAX_SAFE_REVISION } from '../../../packages/shared/src/contracts/gameCreationApp'; import type { ResourceCanvasItem } from '../src/view/project-development/resourceCanvasLayoutModel'; import { createResourceSignature, useProjectResourceCanvasLayout, } from '../src/view/project-development/useProjectResourceCanvasLayout'; const projectId = 'layout-hook-project'; const projectPath = '/tmp/layout-hook-project'; function resource(id: string): ResourceCanvasItem { return { id, category: 'document', subtype: 'agent-result', label: id, mediaType: 'text/markdown', dependencyDepth: 0, }; } function persistedLayout( mode: ProjectResourceCanvasLayoutMode, revision: number, positions: ProjectResourceCanvasPosition[], ): ProjectResourceCanvasLayout { return { schemaVersion: 'game-creator-resource-layout.v1', projectId, mode, revision, positions, updatedAt: revision * 100, }; } function position( resourceId: string, x: number, y: number, ): ProjectResourceCanvasPosition { return { resourceId, section: 'document', x, y, manuallyPlaced: true, }; } function automaticPosition( resourceId: string, x: number, y: number, ): ProjectResourceCanvasPosition { return { ...position(resourceId, x, y), manuallyPlaced: false, }; } afterEach(() => { cleanup(); window.__TAURI__ = undefined; vi.restoreAllMocks(); }); describe('useProjectResourceCanvasLayout', () => { it('includes subtype changes in the resource coordination signature', () => { const original = resource('resource-a'); expect(createResourceSignature([original])).not.toBe( createResourceSignature([{ ...original, subtype: 'task-artifact' }]), ); }); it('waits for dependency graph initialization before reading or writing layout', async () => { const updates: ProjectResourceCanvasPosition[][] = []; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout('dependency', 0, []); } if (command === 'update_local_project_resource_canvas_layout') { const positions = structuredClone( args?.positions as ProjectResourceCanvasPosition[], ); updates.push(positions); return { status: 'updated', layout: persistedLayout('dependency', 1, positions), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const shallowResource = resource('resource-a'); const deepResource = { ...shallowResource, dependencyDepth: 2 }; const { result, rerender } = renderHook( ({ initializationReady, resources }) => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources, initializationReady, rederiveAutomaticPositions: true, }), { initialProps: { initializationReady: false, resources: [shallowResource], }, }, ); expect(invoke).not.toHaveBeenCalled(); expect(result.current.layout.positions).toEqual([]); rerender({ initializationReady: true, resources: [deepResource] }); await waitFor(() => expect(updates).toHaveLength(1)); expect(result.current.layout.positions[0]).toMatchObject({ resourceId: 'resource-a', x: 392, manuallyPlaced: false, }); expect(invoke.mock.calls[0]?.[0]).toBe( 'read_local_project_resource_canvas_layout', ); }); it('persists distinct automatic columns for dependency depths 0, 1, and 2', async () => { const updates: ProjectResourceCanvasPosition[][] = []; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout('dependency', 0, []); } if (command === 'update_local_project_resource_canvas_layout') { const positions = structuredClone( args?.positions as ProjectResourceCanvasPosition[], ); updates.push(positions); return { status: 'updated', layout: persistedLayout('dependency', 1, positions), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderHook(() => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources: [ resource('resource-depth-0'), { ...resource('resource-depth-1'), dependencyDepth: 1 }, { ...resource('resource-depth-2'), dependencyDepth: 2 }, ], rederiveAutomaticPositions: true, }), ); await waitFor(() => expect(updates).toHaveLength(1)); expect(updates[0]).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: 'resource-depth-0', x: 0, y: 0, manuallyPlaced: false, }), expect.objectContaining({ resourceId: 'resource-depth-1', x: 196, y: 0, manuallyPlaced: false, }), expect.objectContaining({ resourceId: 'resource-depth-2', x: 392, y: 0, manuallyPlaced: false, }), ]), ); }); it('rederives automatic dependency positions while preserving manual positions', async () => { const resourceA = { ...resource('resource-a'), dependencyDepth: 2 }; const resourceB = resource('resource-b'); const updates: ProjectResourceCanvasPosition[][] = []; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout('dependency', 4, [ automaticPosition('resource-a', 0, 0), position('resource-b', 600, 40), ]); } if (command === 'update_local_project_resource_canvas_layout') { const positions = structuredClone( args?.positions as ProjectResourceCanvasPosition[], ); updates.push(positions); return { status: 'updated', layout: persistedLayout('dependency', 5, positions), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const { result } = renderHook(() => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources: [resourceA, resourceB], rederiveAutomaticPositions: true, }), ); await waitFor(() => expect(updates).toHaveLength(1)); expect(result.current.layout.positions).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: 'resource-a', x: 392, manuallyPlaced: false, }), expect.objectContaining({ resourceId: 'resource-b', x: 600, y: 40, manuallyPlaced: true, }), ]), ); }); it('rejects an unsafe revision from the initial IPC read without writing', async () => { const resourceA = resource('resource-a'); const invoke = vi.fn(async (command: string) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout( 'dependency', GAME_CREATION_RESOURCE_LAYOUT_MAX_SAFE_REVISION + 1, [position('resource-a', 10, 20)], ); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; const { result } = renderHook(() => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources: [resourceA], }), ); await waitFor(() => expect(result.current.notice).toBe('布局读取失败,已使用当前会话布局'), ); expect( invoke.mock.calls.filter( ([command]) => command === 'update_local_project_resource_canvas_layout', ), ).toHaveLength(0); }); it('rejects an unsafe revision from an update response and restores the trusted layout', async () => { const resourceA = resource('resource-a'); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout('dependency', 1, [ position('resource-a', 10, 20), ]); } if (command === 'update_local_project_resource_canvas_layout') { return { status: 'updated', layout: persistedLayout( 'dependency', GAME_CREATION_RESOURCE_LAYOUT_MAX_SAFE_REVISION + 1, structuredClone( args?.positions as ProjectResourceCanvasPosition[], ), ), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const { result } = renderHook(() => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources: [resourceA], }), ); await waitFor(() => expect(result.current.layout.positions[0]?.x).toBe(10)); act(() => result.current.commitPosition('resource-a', 'document', 100, 30)); await waitFor(() => expect(result.current.notice).toBe('布局保存失败,已恢复上次布局'), ); expect(result.current.layout.positions[0]).toMatchObject({ x: 10, y: 20 }); expect(result.current.saving).toBe(false); }); it('uses the latest resource snapshot when the initial scope read resolves', async () => { const resourceA = resource('resource-a'); const resourceB = resource('resource-b'); let resolveRead: ((layout: ProjectResourceCanvasLayout) => void) | null = null; const updates: Array<{ expectedProjectId: string; expectedRevision: number; positions: ProjectResourceCanvasPosition[]; }> = []; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_canvas_layout') { return await new Promise((resolve) => { resolveRead = resolve; }); } if (command === 'update_local_project_resource_canvas_layout') { const input = args as { expectedProjectId: string; expectedRevision: number; positions: ProjectResourceCanvasPosition[]; }; updates.push(structuredClone(input)); return { status: 'updated', layout: persistedLayout( 'dependency', input.expectedRevision + 1, structuredClone(input.positions), ), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const { result, rerender } = renderHook( ({ resources }: { resources: ResourceCanvasItem[] }) => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources, }), { initialProps: { resources: [resourceA] } }, ); await waitFor(() => expect(resolveRead).not.toBeNull()); rerender({ resources: [resourceA, resourceB] }); await act(async () => { resolveRead?.( persistedLayout('dependency', 1, [position('resource-a', 10, 20)]), ); await Promise.resolve(); }); await waitFor(() => expect(updates).toHaveLength(1)); expect( result.current.layout.positions.map(({ resourceId }) => resourceId), ).toEqual(expect.arrayContaining(['resource-a', 'resource-b'])); expect(updates[0]?.expectedRevision).toBe(1); expect(updates[0]?.expectedProjectId).toBe(projectId); expect( updates[0]?.positions.some( ({ resourceId }) => resourceId === 'resource-b', ), ).toBe(true); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_resource_canvas_layout', ), ).toHaveLength(1); }); it('keeps an in-flight manual CAS alive and serializes resource sync behind its revision', async () => { const resourceA = resource('resource-a'); const resourceB = resource('resource-b'); const updates: Array<{ expectedRevision: number; positions: ProjectResourceCanvasPosition[]; }> = []; let resolveFirstUpdate: | ((result: { status: 'updated'; layout: ProjectResourceCanvasLayout; }) => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout('dependency', 1, [ position('resource-a', 10, 20), ]); } if (command === 'update_local_project_resource_canvas_layout') { const input = args as { expectedRevision: number; positions: ProjectResourceCanvasPosition[]; }; updates.push(structuredClone(input)); if (updates.length === 1) { return await new Promise((resolve) => { resolveFirstUpdate = resolve; }); } return { status: 'updated', layout: persistedLayout( 'dependency', input.expectedRevision + 1, structuredClone(input.positions), ), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const { result, rerender } = renderHook( ({ resources }: { resources: ResourceCanvasItem[] }) => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources, }), { initialProps: { resources: [resourceA] } }, ); await waitFor(() => expect(result.current.layout.positions[0]?.x).toBe(10)); act(() => result.current.commitPosition('resource-a', 'document', 100, 30)); await waitFor(() => expect(updates).toHaveLength(1)); rerender({ resources: [resourceA, resourceB] }); await waitFor(() => expect( result.current.layout.positions.some( ({ resourceId }) => resourceId === 'resource-b', ), ).toBe(true), ); expect(updates).toHaveLength(1); await act(async () => { resolveFirstUpdate?.({ status: 'updated', layout: persistedLayout( 'dependency', 2, structuredClone(updates[0]!.positions), ), }); await Promise.resolve(); }); await waitFor(() => expect(updates).toHaveLength(2)); expect(updates.map(({ expectedRevision }) => expectedRevision)).toEqual([ 1, 2, ]); expect( updates[1]?.positions.some( ({ resourceId }) => resourceId === 'resource-b', ), ).toBe(true); }); it('coalesces repeated queued manual placements for the same resource behind an in-flight CAS', async () => { const resourceA = resource('resource-a'); const updates: Array<{ expectedRevision: number; positions: ProjectResourceCanvasPosition[]; }> = []; let resolveFirstUpdate: | ((result: { status: 'updated'; layout: ProjectResourceCanvasLayout; }) => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout('dependency', 1, [ position('resource-a', 10, 20), ]); } if (command === 'update_local_project_resource_canvas_layout') { const input = args as { expectedRevision: number; positions: ProjectResourceCanvasPosition[]; }; updates.push(structuredClone(input)); if (updates.length === 1) { return await new Promise((resolve) => { resolveFirstUpdate = resolve; }); } return { status: 'updated', layout: persistedLayout( 'dependency', input.expectedRevision + 1, structuredClone(input.positions), ), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const { result } = renderHook(() => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources: [resourceA], }), ); await waitFor(() => expect(result.current.layout.positions[0]?.x).toBe(10)); act(() => result.current.commitPosition('resource-a', 'document', 100, 30)); await waitFor(() => expect(updates).toHaveLength(1)); act(() => { for (let x = 200; x < 300; x += 1) { result.current.commitPosition('resource-a', 'document', x, 40); } }); expect(updates).toHaveLength(1); expect(result.current.layout.positions[0]?.x).toBe(299); await act(async () => { resolveFirstUpdate?.({ status: 'updated', layout: persistedLayout( 'dependency', 2, structuredClone(updates[0]!.positions), ), }); await Promise.resolve(); }); await waitFor(() => expect(updates).toHaveLength(2)); expect(updates.map(({ expectedRevision }) => expectedRevision)).toEqual([ 1, 2, ]); expect(updates[1]?.positions[0]?.x).toBe(299); await waitFor(() => expect(result.current.saving).toBe(false)); }); it('drops manual intents queued behind a CAS conflict', async () => { const resourceA = resource('resource-a'); let updateCalls = 0; let resolveUpdate: | ((result: { status: 'conflict'; layout: ProjectResourceCanvasLayout; }) => void) | null = null; const invoke = vi.fn(async (command: string) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout('dependency', 1, [ position('resource-a', 10, 20), ]); } if (command === 'update_local_project_resource_canvas_layout') { updateCalls += 1; return await new Promise((resolve) => { resolveUpdate = resolve; }); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; const { result } = renderHook(() => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources: [resourceA], }), ); await waitFor(() => expect(result.current.layout.positions[0]?.x).toBe(10)); act(() => { result.current.commitPosition('resource-a', 'document', 100, 30); result.current.commitPosition('resource-a', 'document', 220, 40); }); await waitFor(() => expect(updateCalls).toBe(1)); await act(async () => { resolveUpdate?.({ status: 'conflict', layout: persistedLayout('dependency', 2, [ position('resource-a', 400, 80), ]), }); await Promise.resolve(); }); await waitFor(() => { expect(result.current.layout.positions[0]).toMatchObject({ x: 400, y: 80, }); expect(result.current.saving).toBe(false); }); expect(result.current.notice).toBe('布局已在其他窗口更新'); expect(updateCalls).toBe(1); }); it('keeps the manual conflict notice when resource reconciliation drops a queued manual intent and retries', async () => { const resourceA = resource('resource-a'); const resourceB = resource('resource-b'); const updates: Array<{ expectedRevision: number; positions: ProjectResourceCanvasPosition[]; }> = []; let resolveResourceSync: | ((result: { status: 'conflict'; layout: ProjectResourceCanvasLayout; }) => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout('dependency', 1, [ position('resource-a', 10, 20), ]); } if (command === 'update_local_project_resource_canvas_layout') { const input = args as { expectedRevision: number; positions: ProjectResourceCanvasPosition[]; }; updates.push(structuredClone(input)); if (updates.length === 1) { return await new Promise((resolve) => { resolveResourceSync = resolve; }); } return { status: 'updated', layout: persistedLayout( 'dependency', input.expectedRevision + 1, structuredClone(input.positions), ), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const setTimeoutSpy = vi.spyOn(window, 'setTimeout'); const { result } = renderHook(() => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources: [resourceA, resourceB], }), ); await waitFor(() => expect(updates).toHaveLength(1)); act(() => result.current.commitPosition('resource-a', 'document', 100, 30)); expect(result.current.layout.positions).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: 'resource-a', x: 100, y: 30 }), ]), ); expect(updates).toHaveLength(1); await act(async () => { resolveResourceSync?.({ status: 'conflict', layout: persistedLayout('dependency', 2, [ position('resource-a', 400, 80), ]), }); await Promise.resolve(); }); await waitFor(() => expect(updates).toHaveLength(2)); await waitFor(() => expect(result.current.saving).toBe(false)); expect(updates.map(({ expectedRevision }) => expectedRevision)).toEqual([ 1, 2, ]); expect(updates[1]?.positions).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: 'resource-a', x: 400, y: 80 }), expect.objectContaining({ resourceId: 'resource-b' }), ]), ); expect(updates[1]?.positions).not.toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: 'resource-a', x: 100, y: 30 }), ]), ); expect(result.current.layout.positions).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: 'resource-a', x: 400, y: 80 }), ]), ); expect(result.current.notice).toBe('布局已在其他窗口更新'); expect(setTimeoutSpy.mock.calls.some(([, delay]) => delay === 2400)).toBe( false, ); act(() => result.current.commitPosition('resource-a', 'document', 500, 90)); await waitFor(() => expect(updates).toHaveLength(3)); await waitFor(() => expect(result.current.notice).toBe('布局已保存')); expect(setTimeoutSpy.mock.calls.some(([, delay]) => delay === 2400)).toBe( true, ); expect(updates[2]?.expectedRevision).toBe(3); expect(updates[2]?.positions).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: 'resource-a', x: 500, y: 90 }), ]), ); }); it('ignores a late read from the previous mode', async () => { const resourceA = resource('resource-a'); const readResolvers = new Map< ProjectResourceCanvasLayoutMode, (layout: ProjectResourceCanvasLayout) => void >(); const invoke = vi.fn( async (command: string, args?: Record) => { if (command !== 'read_local_project_resource_canvas_layout') { throw new Error(`unexpected invoke ${command}`); } const mode = args?.mode as ProjectResourceCanvasLayoutMode; return await new Promise((resolve) => { readResolvers.set(mode, resolve); }); }, ); window.__TAURI__ = { core: { invoke } }; const { result, rerender } = renderHook( ({ mode }: { mode: ProjectResourceCanvasLayoutMode }) => useProjectResourceCanvasLayout({ projectPath, projectId, mode, resources: [resourceA], }), { initialProps: { mode: 'dependency' as const } }, ); await waitFor(() => expect(readResolvers.has('dependency')).toBe(true)); rerender({ mode: 'type' }); await waitFor(() => expect(readResolvers.has('type')).toBe(true)); await act(async () => { readResolvers.get('type')?.( persistedLayout('type', 5, [position('resource-a', 200, 50)]), ); await Promise.resolve(); }); await waitFor(() => expect(result.current.layout.mode).toBe('type')); await act(async () => { readResolvers.get('dependency')?.( persistedLayout('dependency', 4, [position('resource-a', 10, 20)]), ); await Promise.resolve(); }); expect(result.current.layout.mode).toBe('type'); expect(result.current.layout.positions[0]).toMatchObject({ x: 200, y: 50 }); }); it('does not let a stuck old-scope write block the new scope queue', async () => { const resourceA = resource('resource-a'); const updates: Array<{ mode: ProjectResourceCanvasLayoutMode; expectedRevision: number; }> = []; let resolveDependencyUpdate: | ((result: { status: 'updated'; layout: ProjectResourceCanvasLayout; }) => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { const mode = args?.mode as ProjectResourceCanvasLayoutMode; if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout(mode, mode === 'dependency' ? 1 : 5, [ position('resource-a', mode === 'dependency' ? 10 : 200, 20), ]); } if (command === 'update_local_project_resource_canvas_layout') { const expectedRevision = args?.expectedRevision as number; updates.push({ mode, expectedRevision }); if (mode === 'dependency') { return await new Promise((resolve) => { resolveDependencyUpdate = resolve; }); } return { status: 'updated', layout: persistedLayout(mode, expectedRevision + 1, [ position('resource-a', 300, 40), ]), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const { result, rerender } = renderHook( ({ mode }: { mode: ProjectResourceCanvasLayoutMode }) => useProjectResourceCanvasLayout({ projectPath, projectId, mode, resources: [resourceA], }), { initialProps: { mode: 'dependency' as const } }, ); await waitFor(() => expect(result.current.layout.positions[0]?.x).toBe(10)); act(() => result.current.commitPosition('resource-a', 'document', 100, 30)); await waitFor(() => expect(updates).toHaveLength(1)); rerender({ mode: 'type' }); await waitFor(() => expect(result.current.layout.mode).toBe('type')); act(() => result.current.commitPosition('resource-a', 'document', 300, 40)); await waitFor(() => expect(updates).toHaveLength(2)); expect(updates[1]).toEqual({ mode: 'type', expectedRevision: 5 }); await waitFor(() => expect(result.current.saving).toBe(false)); expect(result.current.layout.mode).toBe('type'); expect(result.current.layout.positions[0]?.x).toBe(300); await act(async () => { resolveDependencyUpdate?.({ status: 'updated', layout: persistedLayout('dependency', 2, [ position('resource-a', 100, 30), ]), }); await Promise.resolve(); }); expect(updates).toHaveLength(2); expect(result.current.layout.mode).toBe('type'); expect(result.current.layout.positions[0]?.x).toBe(300); }); it('bounds automatic resource reconciliation retries under repeated CAS conflicts', async () => { const resourceA = resource('resource-a'); const resourceB = resource('resource-b'); const expectedRevisions: number[] = []; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_canvas_layout') { return persistedLayout('dependency', 1, [ position('resource-a', 10, 20), ]); } if (command === 'update_local_project_resource_canvas_layout') { const expectedRevision = args?.expectedRevision as number; expectedRevisions.push(expectedRevision); return { status: 'conflict', layout: persistedLayout('dependency', expectedRevision + 1, [ position('resource-a', 10, 20), ]), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const { result } = renderHook(() => useProjectResourceCanvasLayout({ projectPath, projectId, mode: 'dependency', resources: [resourceA, resourceB], }), ); await waitFor(() => expect(expectedRevisions).toHaveLength(3)); await waitFor(() => expect(result.current.saving).toBe(false)); expect(expectedRevisions).toEqual([1, 2, 3]); expect(result.current.notice).toBe('布局已在其他窗口更新'); expect( result.current.layout.positions.some( ({ resourceId }) => resourceId === 'resource-b', ), ).toBe(true); }); });