bug: duplicate .close() call

This commit is contained in:
mathuo 2024-10-31 19:31:11 +00:00
parent ee7cf637bb
commit adaeb16b98
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
5 changed files with 229 additions and 41 deletions

View File

@ -1,4 +1,4 @@
import { fromPartial } from "@total-typescript/shoehorn"; import { fromPartial } from '@total-typescript/shoehorn';
export function setupMockWindow() { export function setupMockWindow() {
const listeners: Record<string, (() => void)[]> = {}; const listeners: Record<string, (() => void)[]> = {};
@ -16,6 +16,14 @@ export function setupMockWindow() {
listener(); listener();
} }
}, },
removeEventListener: (type: string, listener: () => void) => {
if (listeners[type]) {
const index = listeners[type].indexOf(listener);
if (index > -1) {
listeners[type].splice(index, 1);
}
}
},
dispatchEvent: (event: Event) => { dispatchEvent: (event: Event) => {
const items = listeners[event.type]; const items = listeners[event.type];
if (!items) { if (!items) {
@ -24,7 +32,9 @@ export function setupMockWindow() {
items.forEach((item) => item()); items.forEach((item) => item());
}, },
document: document, document: document,
close: jest.fn(), close: () => {
listeners['beforeunload']?.forEach((f) => f());
},
get innerWidth() { get innerWidth() {
return width++; return width++;
}, },

View File

@ -8,7 +8,7 @@ import { PanelUpdateEvent } from '../../panel/types';
import { Orientation } from '../../splitview/splitview'; import { Orientation } from '../../splitview/splitview';
import { CompositeDisposable } from '../../lifecycle'; import { CompositeDisposable } from '../../lifecycle';
import { Emitter } from '../../events'; import { Emitter } from '../../events';
import { IDockviewPanel } from '../../dockview/dockviewPanel'; import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { fireEvent, queryByTestId } from '@testing-library/dom'; import { fireEvent, queryByTestId } from '@testing-library/dom';
import { getPanelData } from '../../dnd/dataTransfer'; import { getPanelData } from '../../dnd/dataTransfer';
@ -116,8 +116,6 @@ describe('dockviewComponent', () => {
} }
}, },
}); });
window.open = jest.fn();
}); });
test('update className', () => { test('update className', () => {
@ -4886,6 +4884,150 @@ describe('dockviewComponent', () => {
]); ]);
}); });
test('popout / floating layouts', async () => {
jest.useRealTimers();
const container = document.createElement('div');
window.open = () => setupMockWindow();
const dockview = new DockviewComponent(container, {
createComponent(options) {
switch (options.name) {
case 'default':
return new PanelContentPartTest(
options.id,
options.name
);
default:
throw new Error(`unsupported`);
}
},
});
dockview.layout(1000, 500);
let panel1 = dockview.addPanel({
id: 'panel_1',
component: 'default',
});
let panel2 = dockview.addPanel({
id: 'panel_2',
component: 'default',
});
let panel3 = dockview.addPanel({
id: 'panel_3',
component: 'default',
});
let panel4 = dockview.addPanel({
id: 'panel_4',
component: 'default',
});
expect(panel1.api.location.type).toBe('grid');
expect(panel2.api.location.type).toBe('grid');
expect(panel3.api.location.type).toBe('grid');
expect(panel4.api.location.type).toBe('grid');
dockview.addFloatingGroup(panel2);
dockview.addFloatingGroup(panel3);
expect(panel1.api.location.type).toBe('grid');
expect(panel2.api.location.type).toBe('floating');
expect(panel3.api.location.type).toBe('floating');
expect(panel4.api.location.type).toBe('grid');
await dockview.addPopoutGroup(panel2);
await dockview.addPopoutGroup(panel4);
expect(panel1.api.location.type).toBe('grid');
expect(panel2.api.location.type).toBe('popout');
expect(panel3.api.location.type).toBe('floating');
expect(panel4.api.location.type).toBe('popout');
const state = dockview.toJSON();
dockview.fromJSON(state);
/**
* exhaust task queue since popout group completion is async but not awaited in `fromJSON(...)`
*/
await new Promise((resolve) => setTimeout(resolve, 0));
expect(dockview.panels.length).toBe(4);
panel1 = dockview.api.getPanel('panel_1') as DockviewPanel;
panel2 = dockview.api.getPanel('panel_2') as DockviewPanel;
panel3 = dockview.api.getPanel('panel_3') as DockviewPanel;
panel4 = dockview.api.getPanel('panel_4') as DockviewPanel;
expect(panel1.api.location.type).toBe('grid');
expect(panel2.api.location.type).toBe('popout');
expect(panel3.api.location.type).toBe('floating');
expect(panel4.api.location.type).toBe('popout');
dockview.clear();
expect(dockview.groups.length).toBe(0);
expect(dockview.panels.length).toBe(0);
});
test('close popout window object', async () => {
const container = document.createElement('div');
const mockWindow = setupMockWindow();
window.open = () => mockWindow;
const dockview = new DockviewComponent(container, {
createComponent(options) {
switch (options.name) {
case 'default':
return new PanelContentPartTest(
options.id,
options.name
);
default:
throw new Error(`unsupported`);
}
},
});
dockview.layout(1000, 500);
let panel1 = dockview.addPanel({
id: 'panel_1',
component: 'default',
});
let panel2 = dockview.addPanel({
id: 'panel_2',
component: 'default',
position: { referencePanel: panel1, direction: 'within' },
});
let panel3 = dockview.addPanel({
id: 'panel_3',
component: 'default',
});
dockview.addFloatingGroup(panel2);
await dockview.addPopoutGroup(panel2);
expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location.type).toBe('popout');
expect(panel3.group.api.location.type).toBe('grid');
mockWindow.close();
expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location.type).toBe('grid');
dockview.clear();
expect(dockview.groups.length).toBe(0);
expect(dockview.panels.length).toBe(0);
});
test('remove all panels from popout group', async () => { test('remove all panels from popout group', async () => {
const container = document.createElement('div'); const container = document.createElement('div');

View File

@ -541,7 +541,6 @@ export class DockviewComponent
addPopoutGroup( addPopoutGroup(
itemToPopout: DockviewPanel | DockviewGroupPanel, itemToPopout: DockviewPanel | DockviewGroupPanel,
options?: { options?: {
skipRemoveGroup?: boolean;
position?: Box; position?: Box;
popoutUrl?: string; popoutUrl?: string;
onDidOpen?: (event: { id: string; window: Window }) => void; onDidOpen?: (event: { id: string; window: Window }) => void;
@ -593,9 +592,12 @@ export class DockviewComponent
} }
); );
let windowExplicitlyClosed = false;
const popoutWindowDisposable = new CompositeDisposable( const popoutWindowDisposable = new CompositeDisposable(
_window, _window,
_window.onDidClose(() => { _window.onDidClose(() => {
windowExplicitlyClosed = true;
popoutWindowDisposable.dispose(); popoutWindowDisposable.dispose();
}) })
); );
@ -627,41 +629,51 @@ export class DockviewComponent
const referenceLocation = itemToPopout.api.location.type; const referenceLocation = itemToPopout.api.location.type;
const group = /**
options?.overridePopoutGroup ?? * The group that is being added doesn't already exist within the DOM, the most likely occurance
this.createGroup({ id: groupId }); * of this case is when being called from the `fromJSON(...)` method
*/
const isGroupAddedToDom =
referenceGroup.element.parentElement !== null;
const group = !isGroupAddedToDom
? referenceGroup
: options?.overridePopoutGroup ??
this.createGroup({ id: groupId });
group.model.renderContainer = overlayRenderContainer; group.model.renderContainer = overlayRenderContainer;
group.layout( group.layout(
_window.window!.innerWidth, _window.window!.innerWidth,
_window.window!.innerHeight _window.window!.innerHeight
); );
if (!options?.overridePopoutGroup) { if (!this._groups.has(group.api.id)) {
this._onDidAddGroup.fire(group); this._onDidAddGroup.fire(group);
} }
if (itemToPopout instanceof DockviewPanel) { if (!options?.overridePopoutGroup && isGroupAddedToDom) {
this.movingLock(() => { if (itemToPopout instanceof DockviewPanel) {
const panel = this.movingLock(() => {
referenceGroup.model.removePanel(itemToPopout); const panel =
group.model.openPanel(panel); referenceGroup.model.removePanel(itemToPopout);
}); group.model.openPanel(panel);
} else { });
this.movingLock(() => } else {
moveGroupWithoutDestroying({ this.movingLock(() =>
from: referenceGroup, moveGroupWithoutDestroying({
to: group, from: referenceGroup,
}) to: group,
); })
);
switch (referenceLocation) { switch (referenceLocation) {
case 'grid': case 'grid':
referenceGroup.api.setVisible(false); referenceGroup.api.setVisible(false);
break; break;
case 'floating': case 'floating':
case 'popout': case 'popout':
this.removeGroup(referenceGroup); this.removeGroup(referenceGroup);
break; break;
}
} }
} }
@ -676,7 +688,10 @@ export class DockviewComponent
getWindow: () => _window.window!, getWindow: () => _window.window!,
}; };
if (itemToPopout.api.location.type === 'grid') { if (
isGroupAddedToDom &&
itemToPopout.api.location.type === 'grid'
) {
itemToPopout.api.setVisible(false); itemToPopout.api.setVisible(false);
} }
@ -698,8 +713,12 @@ export class DockviewComponent
const value = { const value = {
window: _window, window: _window,
popoutGroup: group, popoutGroup: group,
referenceGroup: this.getPanel(referenceGroup.id) referenceGroup: !isGroupAddedToDom
? referenceGroup.id ? undefined
: referenceGroup
? this.getPanel(referenceGroup.id)
? referenceGroup.id
: undefined
: undefined, : undefined,
disposable: { disposable: {
dispose: () => { dispose: () => {
@ -727,7 +746,10 @@ export class DockviewComponent
), ),
overlayRenderContainer, overlayRenderContainer,
Disposable.from(() => { Disposable.from(() => {
if (this.getPanel(referenceGroup.id)) { if (
isGroupAddedToDom &&
this.getPanel(referenceGroup.id)
) {
this.movingLock(() => this.movingLock(() =>
moveGroupWithoutDestroying({ moveGroupWithoutDestroying({
from: group, from: group,
@ -745,14 +767,21 @@ export class DockviewComponent
}); });
} }
} else if (this.getPanel(group.id)) { } else if (this.getPanel(group.id)) {
const removedGroup = this.doRemoveGroup(group, { this.doRemoveGroup(group, {
skipDispose: true, skipDispose: true,
skipActive: true, skipActive: true,
skipPopoutReturn: true,
}); });
const removedGroup = group;
removedGroup.model.renderContainer = removedGroup.model.renderContainer =
this.overlayRenderContainer; this.overlayRenderContainer;
removedGroup.model.location = { type: 'grid' }; removedGroup.model.location = { type: 'grid' };
returnedGroup = removedGroup; returnedGroup = removedGroup;
this.doAddGroup(removedGroup, [0]);
this.doSetGroupAndPanelActive(removedGroup);
} }
}) })
); );
@ -1279,7 +1308,6 @@ export class DockviewComponent
? this.getPanel(gridReferenceGroup) ? this.getPanel(gridReferenceGroup)
: undefined) ?? group, : undefined) ?? group,
{ {
skipRemoveGroup: true,
position: position ?? undefined, position: position ?? undefined,
overridePopoutGroup: gridReferenceGroup overridePopoutGroup: gridReferenceGroup
? group ? group
@ -1299,6 +1327,10 @@ export class DockviewComponent
} }
} }
} catch (err) { } catch (err) {
console.error(
'dockview: failed to deserialize layout. Reverting changes',
err
);
/** /**
* Takes all the successfully created groups and remove all of their panels. * Takes all the successfully created groups and remove all of their panels.
*/ */

View File

@ -59,7 +59,6 @@ export class PopoutWindow extends CompositeDisposable {
}); });
this._window.disposable.dispose(); this._window.disposable.dispose();
this._window.value.close();
this._window = null; this._window = null;
this._onDidClose.fire(); this._onDidClose.fire();

View File

@ -103,7 +103,8 @@ export const GridActions = (props: {
if (state) { if (state) {
try { try {
props.api?.fromJSON(JSON.parse(state)); props.api?.fromJSON(JSON.parse(state));
} catch { } catch (err) {
console.error('failed to load state', err);
localStorage.removeItem('dv-demo-state'); localStorage.removeItem('dv-demo-state');
} }
} }
@ -121,8 +122,12 @@ export const GridActions = (props: {
const onReset = () => { const onReset = () => {
if (props.api) { if (props.api) {
props.api.clear(); try {
defaultConfig(props.api); props.api.clear();
defaultConfig(props.api);
} catch (err) {
localStorage.removeItem('dv-demo-state');
}
} }
}; };