diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 3f39a0096..0e415e81e 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -5810,9 +5810,9 @@ describe('dockviewComponent', () => { dockview.fromJSON(state); /** - * exhaust task queue since popout group completion is async but not awaited in `fromJSON(...)` + * Wait for delayed popout group creation to complete */ - await new Promise((resolve) => setTimeout(resolve, 0)); + await dockview.popoutRestorationPromise; expect(dockview.panels.length).toBe(4); @@ -6113,6 +6113,7 @@ describe('dockviewComponent', () => { }); test('persistance with custom url', async () => { + jest.useFakeTimers(); const container = document.createElement('div'); window.open = () => setupMockWindow(); @@ -6196,7 +6197,12 @@ describe('dockviewComponent', () => { expect(dockview.groups.length).toBe(0); dockview.fromJSON(state); - await new Promise((resolve) => setTimeout(resolve, 0)); // popout views are completed as a promise so must complete microtask-queue + + // Advance timers to trigger delayed popout creation (0ms, 100ms delays) + jest.advanceTimersByTime(200); + + // Wait for the popout restoration to complete + await dockview.popoutRestorationPromise; expect(dockview.toJSON().popoutGroups).toEqual([ { @@ -6230,6 +6236,8 @@ describe('dockviewComponent', () => { url: '/custom.html', }, ]); + + jest.useRealTimers(); }); describe('when browsers block popups', () => { diff --git a/packages/dockview-core/src/constants.ts b/packages/dockview-core/src/constants.ts index c813942d3..b9c24451c 100644 --- a/packages/dockview-core/src/constants.ts +++ b/packages/dockview-core/src/constants.ts @@ -1,3 +1,5 @@ export const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; export const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100, width: 300, height: 300 }; + +export const DESERIALIZATION_POPOUT_DELAY_MS = 100 diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 1002e950c..ee76d815b 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -70,6 +70,7 @@ import { AnchoredBox, AnchorPosition, Box } from '../types'; import { DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_POSITION, + DESERIALIZATION_POPOUT_DELAY_MS, } from '../constants'; import { DockviewPanelRenderer, @@ -351,6 +352,7 @@ export class DockviewComponent disposable: { dispose: () => DockviewGroupPanel | undefined }; }[] = []; private readonly _rootDropTarget: Droptarget; + private _popoutRestorationPromise: Promise = Promise.resolve(); private readonly _onDidRemoveGroup = new Emitter(); readonly onDidRemoveGroup: Event = @@ -407,6 +409,14 @@ export class DockviewComponent return this._floatingGroups; } + /** + * Promise that resolves when all popout groups from the last fromJSON call are restored. + * Useful for tests that need to wait for delayed popout creation. + */ + get popoutRestorationPromise(): Promise { + return this._popoutRestorationPromise; + } + constructor(container: HTMLElement, options: DockviewComponentOptions) { super(container, { proportionalLayout: true, @@ -1522,21 +1532,36 @@ export class DockviewComponent const serializedPopoutGroups = data.popoutGroups ?? []; - for (const serializedPopoutGroup of serializedPopoutGroups) { + // Create a promise that resolves when all popout groups are created + const popoutPromises: Promise[] = []; + + // Queue popup group creation with delays to avoid browser blocking + serializedPopoutGroups.forEach((serializedPopoutGroup, index) => { const { data, position, gridReferenceGroup, url } = serializedPopoutGroup; const group = createGroupFromSerializedState(data); - this.addPopoutGroup(group, { - position: position ?? undefined, - overridePopoutGroup: gridReferenceGroup ? group : undefined, - referenceGroup: gridReferenceGroup - ? this.getPanel(gridReferenceGroup) - : undefined, - popoutUrl: url, + // Add a small delay for each popup after the first to avoid browser popup blocking + const popoutPromise = new Promise((resolve) => { + setTimeout(() => { + this.addPopoutGroup(group, { + position: position ?? undefined, + overridePopoutGroup: gridReferenceGroup ? group : undefined, + referenceGroup: gridReferenceGroup + ? this.getPanel(gridReferenceGroup) + : undefined, + popoutUrl: url, + }); + resolve(); + }, index * DESERIALIZATION_POPOUT_DELAY_MS); // 100ms delay between each popup }); - } + + popoutPromises.push(popoutPromise); + }); + + // Store the promise for tests to wait on + this._popoutRestorationPromise = Promise.all(popoutPromises).then(() => void 0); for (const floatingGroup of this._floatingGroups) { floatingGroup.overlay.setBounds(); @@ -2358,8 +2383,8 @@ export class DockviewComponent } } else if (targetActivePanel) { // Ensure the target group's original active panel remains active - to.model.openPanel(targetActivePanel, { - skipSetGroupActive: true + to.model.openPanel(targetActivePanel, { + skipSetGroupActive: true }); } } else { @@ -2384,13 +2409,13 @@ export class DockviewComponent if (!selectedPopoutGroup) { throw new Error('failed to find popout group'); } - + // Remove from popout groups list to prevent automatic restoration const index = this._popoutGroups.indexOf(selectedPopoutGroup); if (index >= 0) { this._popoutGroups.splice(index, 1); } - + // Clean up the reference group (ghost) if it exists and is hidden if (selectedPopoutGroup.referenceGroup) { const referenceGroup = this.getPanel(selectedPopoutGroup.referenceGroup); @@ -2398,10 +2423,10 @@ export class DockviewComponent this.doRemoveGroup(referenceGroup, { skipActive: true }); } } - + // Manually dispose the window without triggering restoration selectedPopoutGroup.window.dispose(); - + // Update group's location and containers for target if (to.api.location.type === 'grid') { from.model.renderContainer = this.overlayRenderContainer; @@ -2412,7 +2437,7 @@ export class DockviewComponent from.model.dropTargetContainer = this.rootDropTargetContainer; from.model.location = { type: 'floating' }; } - + break; } } @@ -2425,7 +2450,7 @@ export class DockviewComponent referenceLocation, target ); - + // Add to grid for all moves targeting grid location let size: number; @@ -2454,7 +2479,7 @@ export class DockviewComponent ); if (targetFloatingGroup) { const box = targetFloatingGroup.overlay.toJSON(); - + // Calculate position based on available properties let left: number, top: number; if ('left' in box) { @@ -2464,7 +2489,7 @@ export class DockviewComponent } else { left = 50; // Default fallback } - + if ('top' in box) { top = box.top + 50; } else if ('bottom' in box) { @@ -2472,7 +2497,7 @@ export class DockviewComponent } else { top = 50; // Default fallback } - + this.addFloatingGroup(from, { height: box.height, width: box.width,