diff --git a/blocksuite/affine/blocks/frame/src/frame-manager.ts b/blocksuite/affine/blocks/frame/src/frame-manager.ts index ef5b695550..bf15c14bc2 100644 --- a/blocksuite/affine/blocks/frame/src/frame-manager.ts +++ b/blocksuite/affine/blocks/frame/src/frame-manager.ts @@ -241,20 +241,35 @@ export class EdgelessFrameManager extends GfxExtension { surfaceModel.elementAdded.subscribe(({ id, local }) => { const element = surfaceModel.getElementById(id); if (element && local) { - const frame = this.getFrameFromPoint(element.elementBound.center); - - // if the container created with a frame, skip it. - if ( - isGfxGroupCompatibleModel(element) && - frame && - element.hasChild(frame) - ) { - return; - } - - // new element may intended to be added to other group - // so we need to wait for the next microtask to check if the element can be added to the frame + // The entire frame detection logic must be in microtask for timing reasons: + // + // 1. For connectors: When elementAdded fires, connectors have invalid bounds [0,0,0,0] + // because their path/bounds are calculated in a separate microtask of updateConnectorPath by connector-watcher. + // We need to wait for that calculation to complete before frame detection. + // + // 2. For shapes: Although they have valid bounds immediately, processing them in microtask + // ensures consistent timing and allows other initialization to complete first. + // + // 3. Group compatibility: Some elements may need to establish their group relationships + // before being considered for frame membership. + // + // By embedding the entire logic in microtask, we ensure: + // - Connectors have proper bounds calculated (not [0,0,0,0]) + // - getFrameFromPoint() works correctly with valid element centers + // - All element initialization is complete before frame detection queueMicrotask(() => { + const frame = this.getFrameFromPoint(element.elementBound.center); + + // if the container created with a frame, skip it. + if ( + isGfxGroupCompatibleModel(element) && + frame && + element.hasChild(frame) + ) { + return; + } + + // Only add elements that aren't already grouped and have a valid frame if (!element.group && frame) { this.addElementsToFrame(frame, [element]); } diff --git a/blocksuite/integration-test/src/__tests__/edgeless/surface-ref.spec.ts b/blocksuite/integration-test/src/__tests__/edgeless/surface-ref.spec.ts index 2156efb83c..3ff0b6edca 100644 --- a/blocksuite/integration-test/src/__tests__/edgeless/surface-ref.spec.ts +++ b/blocksuite/integration-test/src/__tests__/edgeless/surface-ref.spec.ts @@ -40,6 +40,7 @@ describe('basic', () => { xywh: '[100, 0, 100, 100]', index: service.generateIndex(), })!; + await wait(0); // wait next frame frameId = service.crud.addBlock( 'affine:frame', { diff --git a/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts b/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts index 44c318a5df..e2f2be6e67 100644 --- a/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts +++ b/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts @@ -478,3 +478,46 @@ test('undo/redo should work when change frame background', async ({ page }) => { expect(await getFrameBackground()).not.toBe(prevBackground); } }); + +test('connector and shape created simultaneously with edgeless-auto-complete should both be added to frame', async ({ + page, +}) => { + // Create a larger frame to ensure everything fits + const frameId = await createFrame(page, [50, 50], [650, 650]); + + // Create first shape inside the frame (well within bounds) + const shape1Id = await createShapeElement( + page, + [150, 150], + [250, 250], + Shape.Square + ); + + // Click on the existing shape to start connection + await clickView(page, [200, 200]); + + const autoComplete = page.locator('edgeless-auto-complete'); + const rightArrowButton = autoComplete + .locator('.edgeless-auto-complete-arrow') + .nth(0); + + await rightArrowButton.click(); + + // Wait for async processing + await waitNextFrame(page); + + // Verify that the frame contains 3 children: original shape + new shape + connector + await assertContainerChildCount(page, frameId, 3); + + // Verify by moving the frame - all elements should move together + const frameTitle = page.locator('affine-frame-title'); + await frameTitle.click(); + await dragBetweenViewCoords(page, [60, 60], [110, 110]); + + // Check that the original shape moved with the frame + await assertEdgelessElementBound(page, shape1Id, [200, 200, 100, 100]); + await assertEdgelessElementBound(page, frameId, [100, 100, 600, 600]); + + // Frame should still contain all 3 elements after the move + await assertContainerChildCount(page, frameId, 3); +});