@theia/core 1.74.0-next.9 → 1.74.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/i18n/nls.cs.json +143 -16
- package/i18n/nls.de.json +143 -16
- package/i18n/nls.es.json +143 -16
- package/i18n/nls.fr.json +143 -16
- package/i18n/nls.hu.json +143 -16
- package/i18n/nls.it.json +143 -16
- package/i18n/nls.ja.json +143 -16
- package/i18n/nls.json +143 -16
- package/i18n/nls.ko.json +143 -16
- package/i18n/nls.pl.json +143 -16
- package/i18n/nls.pt-br.json +143 -16
- package/i18n/nls.ru.json +143 -16
- package/i18n/nls.tr.json +143 -16
- package/i18n/nls.zh-cn.json +143 -16
- package/i18n/nls.zh-tw.json +143 -16
- package/lib/browser/browser-clipboard-service.d.ts +3 -0
- package/lib/browser/browser-clipboard-service.d.ts.map +1 -1
- package/lib/browser/browser-clipboard-service.js +8 -1
- package/lib/browser/browser-clipboard-service.js.map +1 -1
- package/lib/browser/catalog.json +505 -46
- package/lib/browser/clipboard-service.d.ts +11 -0
- package/lib/browser/clipboard-service.d.ts.map +1 -1
- package/lib/browser/clipboard-service.js.map +1 -1
- package/lib/browser/common-frontend-contribution.d.ts +3 -0
- package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/common-frontend-contribution.js +22 -10
- package/lib/browser/common-frontend-contribution.js.map +1 -1
- package/lib/browser/common-frontend-contribution.spec.d.ts +2 -0
- package/lib/browser/common-frontend-contribution.spec.d.ts.map +1 -0
- package/lib/browser/common-frontend-contribution.spec.js +71 -0
- package/lib/browser/common-frontend-contribution.spec.js.map +1 -0
- package/lib/browser/frontend-application-contribution.d.ts +1 -1
- package/lib/browser/frontend-application-module.d.ts +2 -2
- package/lib/browser/frontend-application-module.d.ts.map +1 -1
- package/lib/browser/frontend-application-module.js +13 -2
- package/lib/browser/frontend-application-module.js.map +1 -1
- package/lib/browser/frontend-application.d.ts.map +1 -1
- package/lib/browser/frontend-application.js +12 -2
- package/lib/browser/frontend-application.js.map +1 -1
- package/lib/browser/index.d.ts +1 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/keybinding.d.ts.map +1 -1
- package/lib/browser/keybinding.js +12 -7
- package/lib/browser/keybinding.js.map +1 -1
- package/lib/browser/perspective-service.d.ts +173 -0
- package/lib/browser/perspective-service.d.ts.map +1 -0
- package/lib/browser/perspective-service.js +484 -0
- package/lib/browser/perspective-service.js.map +1 -0
- package/lib/browser/perspective-service.spec.d.ts +2 -0
- package/lib/browser/perspective-service.spec.d.ts.map +1 -0
- package/lib/browser/perspective-service.spec.js +1806 -0
- package/lib/browser/perspective-service.spec.js.map +1 -0
- package/lib/browser/preferences/preference-contribution.d.ts.map +1 -1
- package/lib/browser/preferences/preference-contribution.js +9 -1
- package/lib/browser/preferences/preference-contribution.js.map +1 -1
- package/lib/browser/preferences/preference-service.spec.js +63 -0
- package/lib/browser/preferences/preference-service.spec.js.map +1 -1
- package/lib/browser/shell/application-shell.d.ts +7 -0
- package/lib/browser/shell/application-shell.d.ts.map +1 -1
- package/lib/browser/shell/application-shell.js +38 -28
- package/lib/browser/shell/application-shell.js.map +1 -1
- package/lib/browser/shell/shell-layout-restorer.d.ts +9 -0
- package/lib/browser/shell/shell-layout-restorer.d.ts.map +1 -1
- package/lib/browser/shell/shell-layout-restorer.js +124 -14
- package/lib/browser/shell/shell-layout-restorer.js.map +1 -1
- package/lib/browser/shell/shell-layout-restorer.spec.d.ts +2 -0
- package/lib/browser/shell/shell-layout-restorer.spec.d.ts.map +1 -0
- package/lib/browser/shell/shell-layout-restorer.spec.js +539 -0
- package/lib/browser/shell/shell-layout-restorer.spec.js.map +1 -0
- package/lib/browser/shell/view-contribution.d.ts +2 -0
- package/lib/browser/shell/view-contribution.d.ts.map +1 -1
- package/lib/browser/shell/view-contribution.js +5 -1
- package/lib/browser/shell/view-contribution.js.map +1 -1
- package/lib/browser/status-bar/status-bar.d.ts +2 -0
- package/lib/browser/status-bar/status-bar.d.ts.map +1 -1
- package/lib/browser/status-bar/status-bar.js +11 -3
- package/lib/browser/status-bar/status-bar.js.map +1 -1
- package/lib/browser/tree/tree-consistency.spec.js +103 -0
- package/lib/browser/tree/tree-consistency.spec.js.map +1 -1
- package/lib/browser/tree/tree-widget.d.ts +7 -33
- package/lib/browser/tree/tree-widget.d.ts.map +1 -1
- package/lib/browser/tree/tree-widget.js +7 -54
- package/lib/browser/tree/tree-widget.js.map +1 -1
- package/lib/browser/tree/tree.d.ts.map +1 -1
- package/lib/browser/tree/tree.js +6 -0
- package/lib/browser/tree/tree.js.map +1 -1
- package/lib/browser/widgets/index.d.ts +1 -0
- package/lib/browser/widgets/index.d.ts.map +1 -1
- package/lib/browser/widgets/index.js +1 -0
- package/lib/browser/widgets/index.js.map +1 -1
- package/lib/browser/widgets/select-component.d.ts +15 -2
- package/lib/browser/widgets/select-component.d.ts.map +1 -1
- package/lib/browser/widgets/select-component.js +86 -24
- package/lib/browser/widgets/select-component.js.map +1 -1
- package/lib/browser/widgets/tab-bar-tracker.d.ts +23 -0
- package/lib/browser/widgets/tab-bar-tracker.d.ts.map +1 -0
- package/lib/browser/widgets/tab-bar-tracker.js +65 -0
- package/lib/browser/widgets/tab-bar-tracker.js.map +1 -0
- package/lib/browser/widgets/tab-bar-tracker.spec.d.ts +2 -0
- package/lib/browser/widgets/tab-bar-tracker.spec.d.ts.map +1 -0
- package/lib/browser/widgets/tab-bar-tracker.spec.js +109 -0
- package/lib/browser/widgets/tab-bar-tracker.spec.js.map +1 -0
- package/lib/browser/window/default-secondary-window-service.d.ts +2 -0
- package/lib/browser/window/default-secondary-window-service.d.ts.map +1 -1
- package/lib/browser/window/default-secondary-window-service.js +8 -2
- package/lib/browser/window/default-secondary-window-service.js.map +1 -1
- package/lib/browser/window/default-window-service.d.ts +19 -0
- package/lib/browser/window/default-window-service.d.ts.map +1 -1
- package/lib/browser/window/default-window-service.js +34 -5
- package/lib/browser/window/default-window-service.js.map +1 -1
- package/lib/browser/window/default-window-service.spec.js +22 -0
- package/lib/browser/window/default-window-service.spec.js.map +1 -1
- package/lib/browser/window/secondary-window-service.d.ts +7 -0
- package/lib/browser/window/secondary-window-service.d.ts.map +1 -1
- package/lib/browser/window/window-service.d.ts +1 -1
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts +13 -2
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts.map +1 -1
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.js +31 -3
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.js.map +1 -1
- package/lib/common/preferences/index.d.ts +1 -0
- package/lib/common/preferences/index.d.ts.map +1 -1
- package/lib/common/preferences/index.js +1 -0
- package/lib/common/preferences/index.js.map +1 -1
- package/lib/common/preferences/preference-provider.d.ts +15 -0
- package/lib/common/preferences/preference-provider.d.ts.map +1 -1
- package/lib/common/preferences/preference-provider.js +21 -1
- package/lib/common/preferences/preference-provider.js.map +1 -1
- package/lib/common/preferences/preference-provider.spec.js +17 -0
- package/lib/common/preferences/preference-provider.spec.js.map +1 -1
- package/lib/common/preferences/preference-scope.d.ts +6 -1
- package/lib/common/preferences/preference-scope.d.ts.map +1 -1
- package/lib/common/preferences/preference-scope.js +5 -0
- package/lib/common/preferences/preference-scope.js.map +1 -1
- package/lib/common/preferences/preference-scope.spec.js +5 -3
- package/lib/common/preferences/preference-scope.spec.js.map +1 -1
- package/lib/common/preferences/preference-service.d.ts +19 -0
- package/lib/common/preferences/preference-service.d.ts.map +1 -1
- package/lib/common/preferences/preference-service.js +34 -5
- package/lib/common/preferences/preference-service.js.map +1 -1
- package/lib/common/preferences/session-preference-provider.d.ts +17 -0
- package/lib/common/preferences/session-preference-provider.d.ts.map +1 -0
- package/lib/common/preferences/session-preference-provider.js +77 -0
- package/lib/common/preferences/session-preference-provider.js.map +1 -0
- package/lib/common/preferences/session-preference-provider.spec.d.ts +2 -0
- package/lib/common/preferences/session-preference-provider.spec.d.ts.map +1 -0
- package/lib/common/preferences/session-preference-provider.spec.js +71 -0
- package/lib/common/preferences/session-preference-provider.spec.js.map +1 -0
- package/lib/electron-browser/electron-clipboard-service.d.ts +3 -0
- package/lib/electron-browser/electron-clipboard-service.d.ts.map +1 -1
- package/lib/electron-browser/electron-clipboard-service.js +6 -0
- package/lib/electron-browser/electron-clipboard-service.js.map +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.js +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
- package/lib/electron-browser/preload.d.ts.map +1 -1
- package/lib/electron-browser/preload.js +2 -1
- package/lib/electron-browser/preload.js.map +1 -1
- package/lib/electron-browser/window/electron-secondary-window-service.js +1 -1
- package/lib/electron-browser/window/electron-secondary-window-service.js.map +1 -1
- package/lib/electron-browser/window/electron-window-service.js +3 -3
- package/lib/electron-browser/window/electron-window-service.js.map +1 -1
- package/lib/electron-common/electron-api.d.ts +2 -0
- package/lib/electron-common/electron-api.d.ts.map +1 -1
- package/lib/electron-common/electron-api.js +2 -1
- package/lib/electron-common/electron-api.js.map +1 -1
- package/lib/electron-main/electron-api-main.d.ts.map +1 -1
- package/lib/electron-main/electron-api-main.js +43 -0
- package/lib/electron-main/electron-api-main.js.map +1 -1
- package/lib/electron-main/electron-main-application.d.ts.map +1 -1
- package/lib/electron-main/electron-main-application.js +3 -0
- package/lib/electron-main/electron-main-application.js.map +1 -1
- package/lib/electron-node/hosting/electron-backend-hosting-module.d.ts.map +1 -1
- package/lib/electron-node/hosting/electron-backend-hosting-module.js +7 -0
- package/lib/electron-node/hosting/electron-backend-hosting-module.js.map +1 -1
- package/lib/electron-node/hosting/electron-backend-hosting-module.spec.d.ts +2 -0
- package/lib/electron-node/hosting/electron-backend-hosting-module.spec.d.ts.map +1 -0
- package/lib/electron-node/hosting/electron-backend-hosting-module.spec.js +39 -0
- package/lib/electron-node/hosting/electron-backend-hosting-module.spec.js.map +1 -0
- package/lib/node/hosting/backend-hosting-module.d.ts.map +1 -1
- package/lib/node/hosting/backend-hosting-module.js +4 -1
- package/lib/node/hosting/backend-hosting-module.js.map +1 -1
- package/lib/node/hosting/browser-connection-token.d.ts +29 -1
- package/lib/node/hosting/browser-connection-token.d.ts.map +1 -1
- package/lib/node/hosting/browser-connection-token.js +25 -1
- package/lib/node/hosting/browser-connection-token.js.map +1 -1
- package/lib/node/hosting/browser-connection-token.spec.js +70 -0
- package/lib/node/hosting/browser-connection-token.spec.js.map +1 -1
- package/lib/node/index.d.ts +1 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +3 -1
- package/lib/node/index.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/browser-clipboard-service.ts +7 -1
- package/src/browser/clipboard-service.ts +11 -0
- package/src/browser/common-frontend-contribution.spec.ts +84 -0
- package/src/browser/common-frontend-contribution.ts +22 -10
- package/src/browser/frontend-application-contribution.ts +1 -1
- package/src/browser/frontend-application-module.ts +15 -3
- package/src/browser/frontend-application.ts +12 -4
- package/src/browser/index.ts +1 -0
- package/src/browser/keybinding.ts +12 -7
- package/src/browser/perspective-service.spec.ts +2270 -0
- package/src/browser/perspective-service.ts +625 -0
- package/src/browser/preferences/preference-contribution.ts +9 -1
- package/src/browser/preferences/preference-service.spec.ts +72 -0
- package/src/browser/shell/application-shell.ts +41 -26
- package/src/browser/shell/shell-layout-restorer.spec.ts +746 -0
- package/src/browser/shell/shell-layout-restorer.ts +134 -13
- package/src/browser/shell/view-contribution.ts +6 -1
- package/src/browser/status-bar/status-bar.tsx +13 -3
- package/src/browser/style/select-component.css +16 -0
- package/src/browser/style/sidepanel.css +5 -0
- package/src/browser/style/status-bar.css +1 -1
- package/src/browser/tree/tree-consistency.spec.ts +119 -0
- package/src/browser/tree/tree-widget.tsx +12 -78
- package/src/browser/tree/tree.ts +6 -0
- package/src/browser/widgets/index.ts +1 -0
- package/src/browser/widgets/select-component.tsx +90 -24
- package/src/browser/widgets/tab-bar-tracker.spec.ts +121 -0
- package/src/browser/widgets/tab-bar-tracker.ts +68 -0
- package/src/browser/window/default-secondary-window-service.ts +8 -2
- package/src/browser/window/default-window-service.spec.ts +22 -0
- package/src/browser/window/default-window-service.ts +37 -5
- package/src/browser/window/secondary-window-service.ts +7 -0
- package/src/browser/window/window-service.ts +1 -1
- package/src/browser-only/messaging/frontend-only-service-connection-provider.ts +32 -3
- package/src/common/i18n/nls.metadata.json +4149 -888
- package/src/common/preferences/index.ts +1 -0
- package/src/common/preferences/preference-provider.spec.ts +17 -0
- package/src/common/preferences/preference-provider.ts +22 -1
- package/src/common/preferences/preference-scope.spec.ts +5 -3
- package/src/common/preferences/preference-scope.ts +6 -1
- package/src/common/preferences/preference-service.ts +39 -5
- package/src/common/preferences/session-preference-provider.spec.ts +78 -0
- package/src/common/preferences/session-preference-provider.ts +69 -0
- package/src/electron-browser/electron-clipboard-service.ts +5 -0
- package/src/electron-browser/menu/electron-menu-contribution.ts +1 -1
- package/src/electron-browser/menu/electron-menu-style.css +3 -1
- package/src/electron-browser/preload.ts +5 -2
- package/src/electron-browser/window/electron-secondary-window-service.ts +1 -1
- package/src/electron-browser/window/electron-window-service.ts +3 -3
- package/src/electron-common/electron-api.ts +4 -0
- package/src/electron-main/electron-api-main.ts +56 -3
- package/src/electron-main/electron-main-application.ts +3 -0
- package/src/electron-node/hosting/electron-backend-hosting-module.spec.ts +42 -0
- package/src/electron-node/hosting/electron-backend-hosting-module.ts +8 -0
- package/src/node/hosting/backend-hosting-module.ts +5 -2
- package/src/node/hosting/browser-connection-token.spec.ts +92 -0
- package/src/node/hosting/browser-connection-token.ts +45 -1
- package/src/node/index.ts +1 -0
|
@@ -0,0 +1,1806 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const jsdom_1 = require("./test/jsdom");
|
|
19
|
+
const disableJSDOM = (0, jsdom_1.enableJSDOM)();
|
|
20
|
+
const chai_1 = require("chai");
|
|
21
|
+
const sinon = require("sinon");
|
|
22
|
+
const perspective_service_1 = require("./perspective-service");
|
|
23
|
+
const view_contribution_1 = require("./shell/view-contribution");
|
|
24
|
+
const widgets_1 = require("@lumino/widgets");
|
|
25
|
+
disableJSDOM();
|
|
26
|
+
describe('PerspectiveService', () => {
|
|
27
|
+
let service;
|
|
28
|
+
let addWidgetStub;
|
|
29
|
+
let activateWidgetStub;
|
|
30
|
+
let getTabBarForStub;
|
|
31
|
+
let getAreaForStub;
|
|
32
|
+
let getOrCreateWidgetStub;
|
|
33
|
+
let getLayoutDataStub;
|
|
34
|
+
let setLayoutDataStub;
|
|
35
|
+
let collapsePanelStub;
|
|
36
|
+
let widgetAreaResolver;
|
|
37
|
+
let tryGetWidgetStub;
|
|
38
|
+
let getWidgetsStub;
|
|
39
|
+
let mockLogger;
|
|
40
|
+
let testWidget;
|
|
41
|
+
let toTearDown;
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
toTearDown = (0, jsdom_1.enableJSDOM)();
|
|
44
|
+
service = new perspective_service_1.PerspectiveServiceImpl();
|
|
45
|
+
testWidget = new widgets_1.Widget();
|
|
46
|
+
testWidget.id = 'test-widget';
|
|
47
|
+
addWidgetStub = sinon.stub().resolves();
|
|
48
|
+
activateWidgetStub = sinon.stub().resolves(undefined);
|
|
49
|
+
getTabBarForStub = sinon.stub().returns(undefined);
|
|
50
|
+
getAreaForStub = sinon.stub().returns(undefined);
|
|
51
|
+
getOrCreateWidgetStub = sinon.stub().resolves(testWidget);
|
|
52
|
+
getLayoutDataStub = sinon.stub().returns({ mainPanel: {}, bottomPanel: {} });
|
|
53
|
+
setLayoutDataStub = sinon.stub().resolves();
|
|
54
|
+
collapsePanelStub = sinon.stub().resolves();
|
|
55
|
+
widgetAreaResolver = new perspective_service_1.WidgetAreaResolverImpl();
|
|
56
|
+
getWidgetsStub = sinon.stub().returns([]);
|
|
57
|
+
mockLogger = { debug: sinon.stub(), warn: sinon.stub() };
|
|
58
|
+
const mockShell = {
|
|
59
|
+
addWidget: addWidgetStub,
|
|
60
|
+
activateWidget: activateWidgetStub,
|
|
61
|
+
getTabBarFor: getTabBarForStub,
|
|
62
|
+
getAreaFor: getAreaForStub,
|
|
63
|
+
getLayoutData: getLayoutDataStub,
|
|
64
|
+
setLayoutData: setLayoutDataStub,
|
|
65
|
+
collapsePanel: collapsePanelStub,
|
|
66
|
+
getWidgets: getWidgetsStub
|
|
67
|
+
};
|
|
68
|
+
tryGetWidgetStub = sinon.stub().returns(undefined);
|
|
69
|
+
const mockWidgetManager = {
|
|
70
|
+
getOrCreateWidget: getOrCreateWidgetStub,
|
|
71
|
+
tryGetWidget: tryGetWidgetStub
|
|
72
|
+
};
|
|
73
|
+
// Assign mocks via property access since we can't use DI in tests
|
|
74
|
+
service['shell'] = mockShell;
|
|
75
|
+
service['widgetManager'] = mockWidgetManager;
|
|
76
|
+
service['logger'] = mockLogger;
|
|
77
|
+
service['widgetAreaResolver'] = widgetAreaResolver;
|
|
78
|
+
service['appContributions'] = {
|
|
79
|
+
getContributions: () => []
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
sinon.restore();
|
|
84
|
+
toTearDown();
|
|
85
|
+
});
|
|
86
|
+
it('should register a perspective', () => {
|
|
87
|
+
const descriptor = {
|
|
88
|
+
id: 'test',
|
|
89
|
+
label: 'Test',
|
|
90
|
+
viewPlacements: new Map([['widget-a', 'main']])
|
|
91
|
+
};
|
|
92
|
+
service.registerPerspective(descriptor);
|
|
93
|
+
const perspectives = service.getRegisteredPerspectives();
|
|
94
|
+
(0, chai_1.expect)(perspectives).to.have.lengthOf(1);
|
|
95
|
+
(0, chai_1.expect)(perspectives[0].id).to.equal('test');
|
|
96
|
+
(0, chai_1.expect)(perspectives[0].label).to.equal('Test');
|
|
97
|
+
});
|
|
98
|
+
it('should register multiple perspectives', () => {
|
|
99
|
+
service.registerPerspective({
|
|
100
|
+
id: 'perspective-1',
|
|
101
|
+
label: 'Perspective 1',
|
|
102
|
+
viewPlacements: new Map()
|
|
103
|
+
});
|
|
104
|
+
service.registerPerspective({
|
|
105
|
+
id: 'perspective-2',
|
|
106
|
+
label: 'Perspective 2',
|
|
107
|
+
viewPlacements: new Map()
|
|
108
|
+
});
|
|
109
|
+
(0, chai_1.expect)(service.getRegisteredPerspectives()).to.have.lengthOf(2);
|
|
110
|
+
});
|
|
111
|
+
it('should return undefined for active perspective when none is set', () => {
|
|
112
|
+
(0, chai_1.expect)(service.getActivePerspective()).to.be.undefined;
|
|
113
|
+
});
|
|
114
|
+
it('should return undefined from getAreaForView when no perspective is active', () => {
|
|
115
|
+
service.registerPerspective({
|
|
116
|
+
id: 'test',
|
|
117
|
+
label: 'Test',
|
|
118
|
+
viewPlacements: new Map([['widget-a', 'main']])
|
|
119
|
+
});
|
|
120
|
+
(0, chai_1.expect)(service.getAreaForView('widget-a')).to.be.undefined;
|
|
121
|
+
});
|
|
122
|
+
it('should return the override area when a perspective is active', async () => {
|
|
123
|
+
service.registerPerspective({
|
|
124
|
+
id: 'test',
|
|
125
|
+
label: 'Test',
|
|
126
|
+
viewPlacements: new Map([['widget-a', 'main']])
|
|
127
|
+
});
|
|
128
|
+
await service.switchPerspective('test');
|
|
129
|
+
(0, chai_1.expect)(service.getAreaForView('widget-a')).to.equal('main');
|
|
130
|
+
});
|
|
131
|
+
it('should return undefined for views not in the perspective placement map', async () => {
|
|
132
|
+
service.registerPerspective({
|
|
133
|
+
id: 'test',
|
|
134
|
+
label: 'Test',
|
|
135
|
+
viewPlacements: new Map([['widget-a', 'main']])
|
|
136
|
+
});
|
|
137
|
+
await service.switchPerspective('test');
|
|
138
|
+
(0, chai_1.expect)(service.getAreaForView('widget-b')).to.be.undefined;
|
|
139
|
+
});
|
|
140
|
+
it('should switch perspectives and update the active perspective', async () => {
|
|
141
|
+
service.registerPerspective({
|
|
142
|
+
id: 'first',
|
|
143
|
+
label: 'First',
|
|
144
|
+
viewPlacements: new Map()
|
|
145
|
+
});
|
|
146
|
+
service.registerPerspective({
|
|
147
|
+
id: 'second',
|
|
148
|
+
label: 'Second',
|
|
149
|
+
viewPlacements: new Map()
|
|
150
|
+
});
|
|
151
|
+
await service.switchPerspective('first');
|
|
152
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('first');
|
|
153
|
+
await service.switchPerspective('second');
|
|
154
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('second');
|
|
155
|
+
});
|
|
156
|
+
it('should fire onDidChangePerspective event when switching', async () => {
|
|
157
|
+
service.registerPerspective({
|
|
158
|
+
id: 'test',
|
|
159
|
+
label: 'Test',
|
|
160
|
+
viewPlacements: new Map()
|
|
161
|
+
});
|
|
162
|
+
const spy = sinon.spy();
|
|
163
|
+
service.onDidChangePerspective(spy);
|
|
164
|
+
await service.switchPerspective('test');
|
|
165
|
+
(0, chai_1.expect)(spy.calledOnce).to.be.true;
|
|
166
|
+
(0, chai_1.expect)(spy.calledWith('test')).to.be.true;
|
|
167
|
+
});
|
|
168
|
+
it('should not switch to a non-existent perspective', async () => {
|
|
169
|
+
const spy = sinon.spy();
|
|
170
|
+
service.onDidChangePerspective(spy);
|
|
171
|
+
await service.switchPerspective('nonexistent');
|
|
172
|
+
(0, chai_1.expect)(spy.called).to.be.false;
|
|
173
|
+
(0, chai_1.expect)(service.getActivePerspective()).to.be.undefined;
|
|
174
|
+
});
|
|
175
|
+
it('should call onDeactivate on old perspective and onActivate on new perspective', async () => {
|
|
176
|
+
const onDeactivate = sinon.spy();
|
|
177
|
+
const onActivate = sinon.spy();
|
|
178
|
+
service.registerPerspective({
|
|
179
|
+
id: 'old',
|
|
180
|
+
label: 'Old',
|
|
181
|
+
viewPlacements: new Map(),
|
|
182
|
+
onDeactivate
|
|
183
|
+
});
|
|
184
|
+
service.registerPerspective({
|
|
185
|
+
id: 'new',
|
|
186
|
+
label: 'New',
|
|
187
|
+
viewPlacements: new Map(),
|
|
188
|
+
onActivate
|
|
189
|
+
});
|
|
190
|
+
await service.switchPerspective('old');
|
|
191
|
+
await service.switchPerspective('new');
|
|
192
|
+
(0, chai_1.expect)(onDeactivate.calledOnce).to.be.true;
|
|
193
|
+
(0, chai_1.expect)(onActivate.calledOnce).to.be.true;
|
|
194
|
+
(0, chai_1.expect)(onDeactivate.calledBefore(onActivate)).to.be.true;
|
|
195
|
+
});
|
|
196
|
+
it('should add widgets to the target area during switch', async () => {
|
|
197
|
+
service.registerPerspective({
|
|
198
|
+
id: 'test',
|
|
199
|
+
label: 'Test',
|
|
200
|
+
viewPlacements: new Map([['test-widget', 'main']])
|
|
201
|
+
});
|
|
202
|
+
await service.switchPerspective('test');
|
|
203
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('test-widget')).to.be.true;
|
|
204
|
+
(0, chai_1.expect)(addWidgetStub.calledOnce).to.be.true;
|
|
205
|
+
(0, chai_1.expect)(addWidgetStub.calledWith(testWidget, sinon.match({ area: 'main' }))).to.be.true;
|
|
206
|
+
});
|
|
207
|
+
it('should skip adding widget if already in the correct area', async () => {
|
|
208
|
+
getTabBarForStub.returns({});
|
|
209
|
+
getAreaForStub.returns('main');
|
|
210
|
+
service.registerPerspective({
|
|
211
|
+
id: 'test',
|
|
212
|
+
label: 'Test',
|
|
213
|
+
viewPlacements: new Map([['test-widget', 'main']])
|
|
214
|
+
});
|
|
215
|
+
await service.switchPerspective('test');
|
|
216
|
+
(0, chai_1.expect)(addWidgetStub.called).to.be.false;
|
|
217
|
+
});
|
|
218
|
+
it('should call initialize and register contributions', () => {
|
|
219
|
+
const mockContribution = {
|
|
220
|
+
registerPerspectives: sinon.spy()
|
|
221
|
+
};
|
|
222
|
+
service['contributions'] = {
|
|
223
|
+
getContributions: () => [mockContribution]
|
|
224
|
+
};
|
|
225
|
+
service.initialize();
|
|
226
|
+
(0, chai_1.expect)(mockContribution.registerPerspectives.calledOnce).to.be.true;
|
|
227
|
+
(0, chai_1.expect)(mockContribution.registerPerspectives.calledWith(service)).to.be.true;
|
|
228
|
+
});
|
|
229
|
+
it('should handle initialize with no contributions', () => {
|
|
230
|
+
service['contributions'] = undefined;
|
|
231
|
+
(0, chai_1.expect)(() => service.initialize()).to.not.throw();
|
|
232
|
+
});
|
|
233
|
+
// --- New tests for no-op guard, layout save/restore, and default perspective ---
|
|
234
|
+
it('should register the default perspective on initialize', () => {
|
|
235
|
+
service.initialize();
|
|
236
|
+
const perspectives = service.getRegisteredPerspectives();
|
|
237
|
+
const defaultPerspective = perspectives.find(p => p.id === perspective_service_1.PerspectiveServiceImpl.DEFAULT_PERSPECTIVE_ID);
|
|
238
|
+
(0, chai_1.expect)(defaultPerspective).to.not.be.undefined;
|
|
239
|
+
(0, chai_1.expect)(defaultPerspective.label).to.equal('Default');
|
|
240
|
+
(0, chai_1.expect)(defaultPerspective.viewPlacements.size).to.equal(0);
|
|
241
|
+
});
|
|
242
|
+
it('should set the default perspective as the initial active perspective', () => {
|
|
243
|
+
service.initialize();
|
|
244
|
+
const active = service.getActivePerspective();
|
|
245
|
+
(0, chai_1.expect)(active).to.not.be.undefined;
|
|
246
|
+
(0, chai_1.expect)(active.id).to.equal(perspective_service_1.PerspectiveServiceImpl.DEFAULT_PERSPECTIVE_ID);
|
|
247
|
+
});
|
|
248
|
+
it('should not re-apply when switching to the already-active perspective', async () => {
|
|
249
|
+
service.registerPerspective({
|
|
250
|
+
id: 'test',
|
|
251
|
+
label: 'Test',
|
|
252
|
+
viewPlacements: new Map([['test-widget', 'main']]),
|
|
253
|
+
onActivate: sinon.spy(),
|
|
254
|
+
onDeactivate: sinon.spy()
|
|
255
|
+
});
|
|
256
|
+
await service.switchPerspective('test');
|
|
257
|
+
// Reset all stubs
|
|
258
|
+
addWidgetStub.resetHistory();
|
|
259
|
+
setLayoutDataStub.resetHistory();
|
|
260
|
+
getOrCreateWidgetStub.resetHistory();
|
|
261
|
+
getLayoutDataStub.resetHistory();
|
|
262
|
+
const eventSpy = sinon.spy();
|
|
263
|
+
service.onDidChangePerspective(eventSpy);
|
|
264
|
+
const descriptor = service.getActivePerspective();
|
|
265
|
+
const onActivateSpy = descriptor.onActivate;
|
|
266
|
+
const onDeactivateSpy = descriptor.onDeactivate;
|
|
267
|
+
onActivateSpy.resetHistory();
|
|
268
|
+
onDeactivateSpy.resetHistory();
|
|
269
|
+
// Switch to the same perspective again
|
|
270
|
+
await service.switchPerspective('test');
|
|
271
|
+
(0, chai_1.expect)(addWidgetStub.called).to.be.false;
|
|
272
|
+
(0, chai_1.expect)(setLayoutDataStub.called).to.be.false;
|
|
273
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.called).to.be.false;
|
|
274
|
+
(0, chai_1.expect)(getLayoutDataStub.called).to.be.false;
|
|
275
|
+
(0, chai_1.expect)(eventSpy.called).to.be.false;
|
|
276
|
+
(0, chai_1.expect)(onActivateSpy.called).to.be.false;
|
|
277
|
+
(0, chai_1.expect)(onDeactivateSpy.called).to.be.false;
|
|
278
|
+
});
|
|
279
|
+
it('should save layout when switching away from a perspective', async () => {
|
|
280
|
+
service.registerPerspective({
|
|
281
|
+
id: 'perspA',
|
|
282
|
+
label: 'A',
|
|
283
|
+
viewPlacements: new Map()
|
|
284
|
+
});
|
|
285
|
+
service.registerPerspective({
|
|
286
|
+
id: 'perspB',
|
|
287
|
+
label: 'B',
|
|
288
|
+
viewPlacements: new Map()
|
|
289
|
+
});
|
|
290
|
+
await service.switchPerspective('perspA');
|
|
291
|
+
const layoutA = { mainPanel: { widgets: ['editor1'] }, bottomPanel: {} };
|
|
292
|
+
getLayoutDataStub.returns(layoutA);
|
|
293
|
+
// Switch to B — should save A's layout
|
|
294
|
+
await service.switchPerspective('perspB');
|
|
295
|
+
(0, chai_1.expect)(getLayoutDataStub.called).to.be.true;
|
|
296
|
+
const layoutB = { mainPanel: { widgets: ['something-else'] }, bottomPanel: {} };
|
|
297
|
+
getLayoutDataStub.returns(layoutB);
|
|
298
|
+
setLayoutDataStub.resetHistory();
|
|
299
|
+
// Switch back to A — should restore A's saved layout
|
|
300
|
+
await service.switchPerspective('perspA');
|
|
301
|
+
(0, chai_1.expect)(setLayoutDataStub.calledOnce).to.be.true;
|
|
302
|
+
(0, chai_1.expect)(setLayoutDataStub.calledWith(layoutA)).to.be.true;
|
|
303
|
+
});
|
|
304
|
+
it('should restore saved layout when switching back to a perspective', async () => {
|
|
305
|
+
service.registerPerspective({
|
|
306
|
+
id: 'perspA',
|
|
307
|
+
label: 'A',
|
|
308
|
+
viewPlacements: new Map([['test-widget', 'left']])
|
|
309
|
+
});
|
|
310
|
+
service.registerPerspective({
|
|
311
|
+
id: 'perspB',
|
|
312
|
+
label: 'B',
|
|
313
|
+
viewPlacements: new Map()
|
|
314
|
+
});
|
|
315
|
+
// Activate A (first time — applies viewPlacements)
|
|
316
|
+
await service.switchPerspective('perspA');
|
|
317
|
+
const layoutA = { mainPanel: { widgets: ['editor-customized'] }, bottomPanel: {} };
|
|
318
|
+
getLayoutDataStub.returns(layoutA);
|
|
319
|
+
// Switch to B (saves A's layout)
|
|
320
|
+
await service.switchPerspective('perspB');
|
|
321
|
+
const layoutB = { mainPanel: { widgets: ['something-else'] }, bottomPanel: {} };
|
|
322
|
+
getLayoutDataStub.returns(layoutB);
|
|
323
|
+
setLayoutDataStub.resetHistory();
|
|
324
|
+
getOrCreateWidgetStub.resetHistory();
|
|
325
|
+
addWidgetStub.resetHistory();
|
|
326
|
+
// Switch back to A — should restore saved layout, NOT apply viewPlacements
|
|
327
|
+
await service.switchPerspective('perspA');
|
|
328
|
+
(0, chai_1.expect)(setLayoutDataStub.calledOnce).to.be.true;
|
|
329
|
+
(0, chai_1.expect)(setLayoutDataStub.calledWith(layoutA)).to.be.true;
|
|
330
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.called).to.be.false;
|
|
331
|
+
(0, chai_1.expect)(addWidgetStub.called).to.be.false;
|
|
332
|
+
});
|
|
333
|
+
it('should apply viewPlacements on first activation (no saved layout)', async () => {
|
|
334
|
+
service.registerPerspective({
|
|
335
|
+
id: 'fresh',
|
|
336
|
+
label: 'Fresh',
|
|
337
|
+
viewPlacements: new Map([['test-widget', 'main']])
|
|
338
|
+
});
|
|
339
|
+
await service.switchPerspective('fresh');
|
|
340
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('test-widget')).to.be.true;
|
|
341
|
+
(0, chai_1.expect)(addWidgetStub.calledOnce).to.be.true;
|
|
342
|
+
(0, chai_1.expect)(addWidgetStub.calledWith(testWidget, sinon.match({ area: 'main' }))).to.be.true;
|
|
343
|
+
(0, chai_1.expect)(setLayoutDataStub.called).to.be.false;
|
|
344
|
+
});
|
|
345
|
+
it('should not apply viewPlacements when a saved layout exists', async () => {
|
|
346
|
+
service.registerPerspective({
|
|
347
|
+
id: 'perspA',
|
|
348
|
+
label: 'A',
|
|
349
|
+
viewPlacements: new Map([['test-widget', 'left']])
|
|
350
|
+
});
|
|
351
|
+
service.registerPerspective({
|
|
352
|
+
id: 'perspB',
|
|
353
|
+
label: 'B',
|
|
354
|
+
viewPlacements: new Map()
|
|
355
|
+
});
|
|
356
|
+
// First activation of A — viewPlacements applied
|
|
357
|
+
await service.switchPerspective('perspA');
|
|
358
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.called).to.be.true;
|
|
359
|
+
// Switch to B (saves A's layout)
|
|
360
|
+
await service.switchPerspective('perspB');
|
|
361
|
+
getOrCreateWidgetStub.resetHistory();
|
|
362
|
+
addWidgetStub.resetHistory();
|
|
363
|
+
// Switch back to A — should restore layout, NOT use viewPlacements
|
|
364
|
+
await service.switchPerspective('perspA');
|
|
365
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.called).to.be.false;
|
|
366
|
+
(0, chai_1.expect)(addWidgetStub.called).to.be.false;
|
|
367
|
+
(0, chai_1.expect)(setLayoutDataStub.called).to.be.true;
|
|
368
|
+
});
|
|
369
|
+
it('should allow full round-trip: default → custom → default', async () => {
|
|
370
|
+
service.initialize();
|
|
371
|
+
service.registerPerspective({
|
|
372
|
+
id: 'ai-first',
|
|
373
|
+
label: 'AI First',
|
|
374
|
+
viewPlacements: new Map([['test-widget', 'left']])
|
|
375
|
+
});
|
|
376
|
+
// Start in default (set by initialize)
|
|
377
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal(perspective_service_1.PerspectiveServiceImpl.DEFAULT_PERSPECTIVE_ID);
|
|
378
|
+
const defaultLayout = { mainPanel: { widgets: ['default-editor'] }, bottomPanel: {} };
|
|
379
|
+
getLayoutDataStub.returns(defaultLayout);
|
|
380
|
+
// Switch to ai-first (saves default layout)
|
|
381
|
+
await service.switchPerspective('ai-first');
|
|
382
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('ai-first');
|
|
383
|
+
(0, chai_1.expect)(getLayoutDataStub.called).to.be.true;
|
|
384
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('test-widget')).to.be.true;
|
|
385
|
+
const aiLayout = { mainPanel: { widgets: ['ai-stuff'] }, bottomPanel: {} };
|
|
386
|
+
getLayoutDataStub.returns(aiLayout);
|
|
387
|
+
setLayoutDataStub.resetHistory();
|
|
388
|
+
// Switch back to default (saves ai-first layout, restores default layout)
|
|
389
|
+
await service.switchPerspective(perspective_service_1.PerspectiveServiceImpl.DEFAULT_PERSPECTIVE_ID);
|
|
390
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal(perspective_service_1.PerspectiveServiceImpl.DEFAULT_PERSPECTIVE_ID);
|
|
391
|
+
(0, chai_1.expect)(setLayoutDataStub.calledOnce).to.be.true;
|
|
392
|
+
(0, chai_1.expect)(setLayoutDataStub.calledWith(defaultLayout)).to.be.true;
|
|
393
|
+
setLayoutDataStub.resetHistory();
|
|
394
|
+
// Switch back to ai-first (saves default layout again, restores ai-first layout)
|
|
395
|
+
await service.switchPerspective('ai-first');
|
|
396
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('ai-first');
|
|
397
|
+
(0, chai_1.expect)(setLayoutDataStub.calledOnce).to.be.true;
|
|
398
|
+
(0, chai_1.expect)(setLayoutDataStub.calledWith(aiLayout)).to.be.true;
|
|
399
|
+
});
|
|
400
|
+
// --- Chrome control options tests ---
|
|
401
|
+
it('should collapse areas on first activation only', async () => {
|
|
402
|
+
service.registerPerspective({
|
|
403
|
+
id: 'collapse-test',
|
|
404
|
+
label: 'Collapse Test',
|
|
405
|
+
viewPlacements: new Map(),
|
|
406
|
+
chromeOptions: { collapseAreas: ['left'] }
|
|
407
|
+
});
|
|
408
|
+
service.registerPerspective({
|
|
409
|
+
id: 'other',
|
|
410
|
+
label: 'Other',
|
|
411
|
+
viewPlacements: new Map()
|
|
412
|
+
});
|
|
413
|
+
// First activation — should collapse
|
|
414
|
+
await service.switchPerspective('collapse-test');
|
|
415
|
+
(0, chai_1.expect)(collapsePanelStub.calledOnce).to.be.true;
|
|
416
|
+
(0, chai_1.expect)(collapsePanelStub.calledWith('left')).to.be.true;
|
|
417
|
+
collapsePanelStub.resetHistory();
|
|
418
|
+
// Switch away (saves layout)
|
|
419
|
+
await service.switchPerspective('other');
|
|
420
|
+
collapsePanelStub.resetHistory();
|
|
421
|
+
// Switch back — should restore saved layout, NOT collapse again
|
|
422
|
+
await service.switchPerspective('collapse-test');
|
|
423
|
+
(0, chai_1.expect)(collapsePanelStub.called).to.be.false;
|
|
424
|
+
});
|
|
425
|
+
it('should collapse multiple areas on first activation', async () => {
|
|
426
|
+
service.registerPerspective({
|
|
427
|
+
id: 'multi-collapse',
|
|
428
|
+
label: 'Multi Collapse',
|
|
429
|
+
viewPlacements: new Map(),
|
|
430
|
+
chromeOptions: { collapseAreas: ['left', 'bottom'] }
|
|
431
|
+
});
|
|
432
|
+
await service.switchPerspective('multi-collapse');
|
|
433
|
+
(0, chai_1.expect)(collapsePanelStub.calledTwice).to.be.true;
|
|
434
|
+
(0, chai_1.expect)(collapsePanelStub.calledWith('left')).to.be.true;
|
|
435
|
+
(0, chai_1.expect)(collapsePanelStub.calledWith('bottom')).to.be.true;
|
|
436
|
+
});
|
|
437
|
+
it('should not collapse areas when perspective has no collapseAreas', async () => {
|
|
438
|
+
service.registerPerspective({
|
|
439
|
+
id: 'no-collapse',
|
|
440
|
+
label: 'No Collapse',
|
|
441
|
+
viewPlacements: new Map()
|
|
442
|
+
});
|
|
443
|
+
await service.switchPerspective('no-collapse');
|
|
444
|
+
(0, chai_1.expect)(collapsePanelStub.called).to.be.false;
|
|
445
|
+
});
|
|
446
|
+
// --- WidgetAreaResolver tests ---
|
|
447
|
+
it('should resolve widget area from active perspective via the WidgetAreaResolver', async () => {
|
|
448
|
+
service.initialize();
|
|
449
|
+
service.registerPerspective({
|
|
450
|
+
id: 'resolver-test',
|
|
451
|
+
label: 'Resolver Test',
|
|
452
|
+
viewPlacements: new Map([['my-widget', 'right']])
|
|
453
|
+
});
|
|
454
|
+
await service.switchPerspective('resolver-test');
|
|
455
|
+
(0, chai_1.expect)(widgetAreaResolver.resolveArea('my-widget', 'left')).to.equal('right');
|
|
456
|
+
});
|
|
457
|
+
it('should return undefined from the resolver for unmapped widgets', async () => {
|
|
458
|
+
service.initialize();
|
|
459
|
+
service.registerPerspective({
|
|
460
|
+
id: 'resolver-test',
|
|
461
|
+
label: 'Resolver Test',
|
|
462
|
+
viewPlacements: new Map([['my-widget', 'right']])
|
|
463
|
+
});
|
|
464
|
+
await service.switchPerspective('resolver-test');
|
|
465
|
+
(0, chai_1.expect)(widgetAreaResolver.resolveArea('unknown-widget', 'main')).to.be.undefined;
|
|
466
|
+
});
|
|
467
|
+
it('should return undefined from the resolver when default perspective is active', () => {
|
|
468
|
+
service.initialize();
|
|
469
|
+
// Default perspective has empty viewPlacements
|
|
470
|
+
(0, chai_1.expect)(widgetAreaResolver.resolveArea('any-widget', 'main')).to.be.undefined;
|
|
471
|
+
});
|
|
472
|
+
// --- Logger tests ---
|
|
473
|
+
it('should log warning when widget creation fails during switchPerspective', async () => {
|
|
474
|
+
const widgetError = new Error('No factory registered');
|
|
475
|
+
getOrCreateWidgetStub.rejects(widgetError);
|
|
476
|
+
service.registerPerspective({
|
|
477
|
+
id: 'fail-test',
|
|
478
|
+
label: 'Fail Test',
|
|
479
|
+
viewPlacements: new Map([['missing-widget', 'main']])
|
|
480
|
+
});
|
|
481
|
+
await service.switchPerspective('fail-test');
|
|
482
|
+
(0, chai_1.expect)(mockLogger.warn.calledOnce).to.be.true;
|
|
483
|
+
(0, chai_1.expect)(mockLogger.warn.firstCall.args[0]).to.equal('Failed to create or place widget for perspective');
|
|
484
|
+
(0, chai_1.expect)(mockLogger.warn.firstCall.args[1]).to.equal(widgetError);
|
|
485
|
+
});
|
|
486
|
+
it('should log warning when widget activation fails during switchPerspective', async () => {
|
|
487
|
+
const activationError = new Error('Activation failed');
|
|
488
|
+
activateWidgetStub.rejects(activationError);
|
|
489
|
+
service.registerPerspective({
|
|
490
|
+
id: 'activate-fail',
|
|
491
|
+
label: 'Activate Fail',
|
|
492
|
+
viewPlacements: new Map([['test-widget', 'main']])
|
|
493
|
+
});
|
|
494
|
+
await service.switchPerspective('activate-fail');
|
|
495
|
+
(0, chai_1.expect)(mockLogger.warn.called).to.be.true;
|
|
496
|
+
const activateCall = mockLogger.warn.getCalls().find((c) => c.args[0] === 'Failed to activate widget for perspective');
|
|
497
|
+
(0, chai_1.expect)(activateCall).to.not.be.undefined;
|
|
498
|
+
(0, chai_1.expect)(activateCall.args[1]).to.equal(activationError);
|
|
499
|
+
});
|
|
500
|
+
// --- Reentrancy guard tests ---
|
|
501
|
+
it('should handle two rapid switchPerspective calls without interleaving', async () => {
|
|
502
|
+
service.registerPerspective({
|
|
503
|
+
id: 'first',
|
|
504
|
+
label: 'First',
|
|
505
|
+
viewPlacements: new Map()
|
|
506
|
+
});
|
|
507
|
+
service.registerPerspective({
|
|
508
|
+
id: 'second',
|
|
509
|
+
label: 'Second',
|
|
510
|
+
viewPlacements: new Map()
|
|
511
|
+
});
|
|
512
|
+
const promise1 = service.switchPerspective('first');
|
|
513
|
+
const promise2 = service.switchPerspective('second');
|
|
514
|
+
await Promise.all([promise1, promise2]);
|
|
515
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('second');
|
|
516
|
+
});
|
|
517
|
+
it('should serialize perspective switches with reentrancy guard', async () => {
|
|
518
|
+
const callOrder = [];
|
|
519
|
+
service.registerPerspective({
|
|
520
|
+
id: 'perspA',
|
|
521
|
+
label: 'A',
|
|
522
|
+
viewPlacements: new Map(),
|
|
523
|
+
onActivate: () => callOrder.push('activate-A')
|
|
524
|
+
});
|
|
525
|
+
service.registerPerspective({
|
|
526
|
+
id: 'perspB',
|
|
527
|
+
label: 'B',
|
|
528
|
+
viewPlacements: new Map(),
|
|
529
|
+
onActivate: () => callOrder.push('activate-B')
|
|
530
|
+
});
|
|
531
|
+
const p1 = service.switchPerspective('perspA');
|
|
532
|
+
const p2 = service.switchPerspective('perspB');
|
|
533
|
+
await Promise.all([p1, p2]);
|
|
534
|
+
(0, chai_1.expect)(callOrder).to.deep.equal(['activate-A', 'activate-B']);
|
|
535
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('perspB');
|
|
536
|
+
});
|
|
537
|
+
it('should serialize three or more concurrent perspective switches', async () => {
|
|
538
|
+
const callOrder = [];
|
|
539
|
+
service.registerPerspective({
|
|
540
|
+
id: 'perspA',
|
|
541
|
+
label: 'A',
|
|
542
|
+
viewPlacements: new Map(),
|
|
543
|
+
onActivate: () => callOrder.push('activate-A')
|
|
544
|
+
});
|
|
545
|
+
service.registerPerspective({
|
|
546
|
+
id: 'perspB',
|
|
547
|
+
label: 'B',
|
|
548
|
+
viewPlacements: new Map(),
|
|
549
|
+
onActivate: () => callOrder.push('activate-B')
|
|
550
|
+
});
|
|
551
|
+
service.registerPerspective({
|
|
552
|
+
id: 'perspC',
|
|
553
|
+
label: 'C',
|
|
554
|
+
viewPlacements: new Map(),
|
|
555
|
+
onActivate: () => callOrder.push('activate-C')
|
|
556
|
+
});
|
|
557
|
+
const p1 = service.switchPerspective('perspA');
|
|
558
|
+
const p2 = service.switchPerspective('perspB');
|
|
559
|
+
const p3 = service.switchPerspective('perspC');
|
|
560
|
+
await Promise.all([p1, p2, p3]);
|
|
561
|
+
(0, chai_1.expect)(callOrder).to.deep.equal(['activate-A', 'activate-B', 'activate-C']);
|
|
562
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('perspC');
|
|
563
|
+
});
|
|
564
|
+
// --- onLayoutRestored() tests ---
|
|
565
|
+
it('should not throw when onLayoutRestored is called with an unregistered perspective', () => {
|
|
566
|
+
service.initialize();
|
|
567
|
+
(0, chai_1.expect)(() => service.onLayoutRestored('non-existent')).to.not.throw();
|
|
568
|
+
});
|
|
569
|
+
// --- Rejection resilience tests ---
|
|
570
|
+
it('should warn and continue when setLayoutData rejects during saved layout restore', async () => {
|
|
571
|
+
service.registerPerspective({
|
|
572
|
+
id: 'perspA',
|
|
573
|
+
label: 'A',
|
|
574
|
+
viewPlacements: new Map()
|
|
575
|
+
});
|
|
576
|
+
service.registerPerspective({
|
|
577
|
+
id: 'perspB',
|
|
578
|
+
label: 'B',
|
|
579
|
+
viewPlacements: new Map()
|
|
580
|
+
});
|
|
581
|
+
// Switch to A, then to B (saves A's layout)
|
|
582
|
+
await service.switchPerspective('perspA');
|
|
583
|
+
await service.switchPerspective('perspB');
|
|
584
|
+
// Make setLayoutData reject
|
|
585
|
+
setLayoutDataStub.rejects(new Error('layout error'));
|
|
586
|
+
const eventSpy = sinon.spy();
|
|
587
|
+
service.onDidChangePerspective(eventSpy);
|
|
588
|
+
// Switch back to A — should not throw
|
|
589
|
+
await service.switchPerspective('perspA');
|
|
590
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('perspA');
|
|
591
|
+
(0, chai_1.expect)(eventSpy.calledOnce).to.be.true;
|
|
592
|
+
(0, chai_1.expect)(eventSpy.calledWith('perspA')).to.be.true;
|
|
593
|
+
(0, chai_1.expect)(mockLogger.warn.calledOnce).to.be.true;
|
|
594
|
+
(0, chai_1.expect)(mockLogger.warn.firstCall.args[0]).to.equal('Failed to apply layout for perspective');
|
|
595
|
+
});
|
|
596
|
+
it('should warn and continue when collapsePanel rejects on first activation', async () => {
|
|
597
|
+
service.registerPerspective({
|
|
598
|
+
id: 'collapse-fail',
|
|
599
|
+
label: 'Collapse Fail',
|
|
600
|
+
viewPlacements: new Map(),
|
|
601
|
+
chromeOptions: { collapseAreas: ['left'] }
|
|
602
|
+
});
|
|
603
|
+
collapsePanelStub.rejects(new Error('collapse error'));
|
|
604
|
+
const eventSpy = sinon.spy();
|
|
605
|
+
service.onDidChangePerspective(eventSpy);
|
|
606
|
+
// Switch — should not throw
|
|
607
|
+
await service.switchPerspective('collapse-fail');
|
|
608
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('collapse-fail');
|
|
609
|
+
(0, chai_1.expect)(eventSpy.calledOnce).to.be.true;
|
|
610
|
+
(0, chai_1.expect)(mockLogger.warn.calledOnce).to.be.true;
|
|
611
|
+
(0, chai_1.expect)(mockLogger.warn.firstCall.args[0]).to.equal('Failed to apply layout for perspective');
|
|
612
|
+
});
|
|
613
|
+
it('should not drop queued switches when layout application fails', async () => {
|
|
614
|
+
service.registerPerspective({
|
|
615
|
+
id: 'perspA',
|
|
616
|
+
label: 'A',
|
|
617
|
+
viewPlacements: new Map()
|
|
618
|
+
});
|
|
619
|
+
service.registerPerspective({
|
|
620
|
+
id: 'perspB',
|
|
621
|
+
label: 'B',
|
|
622
|
+
viewPlacements: new Map()
|
|
623
|
+
});
|
|
624
|
+
service.registerPerspective({
|
|
625
|
+
id: 'perspC',
|
|
626
|
+
label: 'C',
|
|
627
|
+
viewPlacements: new Map()
|
|
628
|
+
});
|
|
629
|
+
// Switch to A, then to B (saves A's layout)
|
|
630
|
+
await service.switchPerspective('perspA');
|
|
631
|
+
await service.switchPerspective('perspB');
|
|
632
|
+
// Make setLayoutData reject only on first call, then resolve
|
|
633
|
+
setLayoutDataStub.resetBehavior();
|
|
634
|
+
setLayoutDataStub.onFirstCall().rejects(new Error('layout error'));
|
|
635
|
+
setLayoutDataStub.onSecondCall().resolves();
|
|
636
|
+
// Queue: switch to A (will fail layout restore), then switch to C
|
|
637
|
+
const p1 = service.switchPerspective('perspA');
|
|
638
|
+
const p2 = service.switchPerspective('perspC');
|
|
639
|
+
await Promise.all([p1, p2]);
|
|
640
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('perspC');
|
|
641
|
+
});
|
|
642
|
+
// --- resetCurrentPerspective tests ---
|
|
643
|
+
describe('resetCurrentPerspective', () => {
|
|
644
|
+
it('should clear saved layout and re-apply viewPlacements on reset for non-default perspective', async () => {
|
|
645
|
+
service.registerPerspective({
|
|
646
|
+
id: 'perspA',
|
|
647
|
+
label: 'A',
|
|
648
|
+
viewPlacements: new Map([['test-widget', 'left']])
|
|
649
|
+
});
|
|
650
|
+
service.registerPerspective({
|
|
651
|
+
id: 'perspB',
|
|
652
|
+
label: 'B',
|
|
653
|
+
viewPlacements: new Map()
|
|
654
|
+
});
|
|
655
|
+
await service.switchPerspective('perspA');
|
|
656
|
+
const layoutA = { mainPanel: { widgets: ['customized'] }, bottomPanel: {} };
|
|
657
|
+
getLayoutDataStub.returns(layoutA);
|
|
658
|
+
// Switch to B (saves A's layout)
|
|
659
|
+
await service.switchPerspective('perspB');
|
|
660
|
+
// Switch back to A (restores saved layout)
|
|
661
|
+
await service.switchPerspective('perspA');
|
|
662
|
+
// Verify saved layout exists
|
|
663
|
+
(0, chai_1.expect)(service.getSavedLayout('perspA')).to.not.be.undefined;
|
|
664
|
+
// Reset stubs to track reset behavior
|
|
665
|
+
setLayoutDataStub.resetHistory();
|
|
666
|
+
getOrCreateWidgetStub.resetHistory();
|
|
667
|
+
addWidgetStub.resetHistory();
|
|
668
|
+
activateWidgetStub.resetHistory();
|
|
669
|
+
await service.resetCurrentPerspective();
|
|
670
|
+
// Saved layout should be cleared
|
|
671
|
+
(0, chai_1.expect)(service.getSavedLayout('perspA')).to.be.undefined;
|
|
672
|
+
// Should re-apply viewPlacements
|
|
673
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('test-widget')).to.be.true;
|
|
674
|
+
(0, chai_1.expect)(addWidgetStub.called).to.be.true;
|
|
675
|
+
});
|
|
676
|
+
it('should call onActivate callback', async () => {
|
|
677
|
+
const onActivate = sinon.spy();
|
|
678
|
+
service.registerPerspective({
|
|
679
|
+
id: 'activate-reset',
|
|
680
|
+
label: 'Activate Reset',
|
|
681
|
+
viewPlacements: new Map(),
|
|
682
|
+
onActivate
|
|
683
|
+
});
|
|
684
|
+
await service.switchPerspective('activate-reset');
|
|
685
|
+
onActivate.resetHistory();
|
|
686
|
+
await service.resetCurrentPerspective();
|
|
687
|
+
(0, chai_1.expect)(onActivate.calledOnce).to.be.true;
|
|
688
|
+
});
|
|
689
|
+
it('should fire onDidChangePerspective event', async () => {
|
|
690
|
+
service.registerPerspective({
|
|
691
|
+
id: 'event-reset',
|
|
692
|
+
label: 'Event Reset',
|
|
693
|
+
viewPlacements: new Map()
|
|
694
|
+
});
|
|
695
|
+
await service.switchPerspective('event-reset');
|
|
696
|
+
const spy = sinon.spy();
|
|
697
|
+
service.onDidChangePerspective(spy);
|
|
698
|
+
await service.resetCurrentPerspective();
|
|
699
|
+
(0, chai_1.expect)(spy.calledOnce).to.be.true;
|
|
700
|
+
(0, chai_1.expect)(spy.calledWith('event-reset')).to.be.true;
|
|
701
|
+
});
|
|
702
|
+
it('should not affect other perspectives\' saved layouts', async () => {
|
|
703
|
+
service.registerPerspective({
|
|
704
|
+
id: 'perspA',
|
|
705
|
+
label: 'A',
|
|
706
|
+
viewPlacements: new Map()
|
|
707
|
+
});
|
|
708
|
+
service.registerPerspective({
|
|
709
|
+
id: 'perspB',
|
|
710
|
+
label: 'B',
|
|
711
|
+
viewPlacements: new Map()
|
|
712
|
+
});
|
|
713
|
+
// Activate A, then B, to create saved layouts for both
|
|
714
|
+
await service.switchPerspective('perspA');
|
|
715
|
+
const layoutA = { mainPanel: { widgets: ['a-stuff'] }, bottomPanel: {} };
|
|
716
|
+
getLayoutDataStub.returns(layoutA);
|
|
717
|
+
await service.switchPerspective('perspB');
|
|
718
|
+
const layoutB = { mainPanel: { widgets: ['b-stuff'] }, bottomPanel: {} };
|
|
719
|
+
getLayoutDataStub.returns(layoutB);
|
|
720
|
+
// Switch back to A (saves B's layout)
|
|
721
|
+
await service.switchPerspective('perspA');
|
|
722
|
+
// Reset A
|
|
723
|
+
await service.resetCurrentPerspective();
|
|
724
|
+
// A's saved layout should be cleared
|
|
725
|
+
(0, chai_1.expect)(service.getSavedLayout('perspA')).to.be.undefined;
|
|
726
|
+
// B's saved layout should be preserved
|
|
727
|
+
(0, chai_1.expect)(service.getSavedLayout('perspB')).to.equal(layoutB);
|
|
728
|
+
});
|
|
729
|
+
it('should serialize with other switchPerspective calls', async () => {
|
|
730
|
+
const callOrder = [];
|
|
731
|
+
service.registerPerspective({
|
|
732
|
+
id: 'perspA',
|
|
733
|
+
label: 'A',
|
|
734
|
+
viewPlacements: new Map(),
|
|
735
|
+
onActivate: () => callOrder.push('activate-A')
|
|
736
|
+
});
|
|
737
|
+
service.registerPerspective({
|
|
738
|
+
id: 'perspB',
|
|
739
|
+
label: 'B',
|
|
740
|
+
viewPlacements: new Map(),
|
|
741
|
+
onActivate: () => callOrder.push('activate-B')
|
|
742
|
+
});
|
|
743
|
+
await service.switchPerspective('perspA');
|
|
744
|
+
callOrder.length = 0;
|
|
745
|
+
// Queue a reset and a switch concurrently
|
|
746
|
+
const p1 = service.resetCurrentPerspective();
|
|
747
|
+
const p2 = service.switchPerspective('perspB');
|
|
748
|
+
await Promise.all([p1, p2]);
|
|
749
|
+
(0, chai_1.expect)(callOrder).to.deep.equal(['activate-A', 'activate-B']);
|
|
750
|
+
(0, chai_1.expect)(service.getActivePerspective()?.id).to.equal('perspB');
|
|
751
|
+
});
|
|
752
|
+
it('should do nothing when no active perspective is set', async () => {
|
|
753
|
+
// No perspective active (activePerspectiveId is undefined)
|
|
754
|
+
const spy = sinon.spy();
|
|
755
|
+
service.onDidChangePerspective(spy);
|
|
756
|
+
await service.resetCurrentPerspective();
|
|
757
|
+
(0, chai_1.expect)(spy.called).to.be.false;
|
|
758
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.called).to.be.false;
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
describe('resetDefaultPerspective', () => {
|
|
762
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
763
|
+
function createMockViewContribution(options) {
|
|
764
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
765
|
+
const contribution = Object.create(view_contribution_1.AbstractViewContribution.prototype);
|
|
766
|
+
contribution.options = options;
|
|
767
|
+
contribution.widgetManager = {
|
|
768
|
+
getOrCreateWidget: getOrCreateWidgetStub,
|
|
769
|
+
tryGetWidget: tryGetWidgetStub
|
|
770
|
+
};
|
|
771
|
+
contribution.shell = service['shell'];
|
|
772
|
+
contribution.openView = sinon.stub().resolves(testWidget);
|
|
773
|
+
return contribution;
|
|
774
|
+
}
|
|
775
|
+
function createMockNonViewContribution() {
|
|
776
|
+
const contrib = {};
|
|
777
|
+
return contrib;
|
|
778
|
+
}
|
|
779
|
+
it('should relocate open view from wrong area to defaultViewOptions.area', async () => {
|
|
780
|
+
service.initialize();
|
|
781
|
+
const explorerWidget = new widgets_1.Widget();
|
|
782
|
+
explorerWidget.id = 'explorer';
|
|
783
|
+
const explorerPanel = new widgets_1.Panel();
|
|
784
|
+
widgets_1.Widget.attach(explorerPanel, document.body);
|
|
785
|
+
explorerPanel.addWidget(explorerWidget);
|
|
786
|
+
tryGetWidgetStub.withArgs('explorer').returns(explorerWidget);
|
|
787
|
+
getAreaForStub.withArgs(explorerWidget).returns('right');
|
|
788
|
+
const contribution = createMockViewContribution({
|
|
789
|
+
widgetId: 'explorer',
|
|
790
|
+
widgetName: 'Explorer',
|
|
791
|
+
defaultWidgetOptions: { area: 'left' }
|
|
792
|
+
});
|
|
793
|
+
service['appContributions'] = {
|
|
794
|
+
getContributions: () => [contribution]
|
|
795
|
+
};
|
|
796
|
+
await service.resetCurrentPerspective();
|
|
797
|
+
(0, chai_1.expect)(addWidgetStub.calledWith(explorerWidget, sinon.match({ area: 'left' }))).to.be.true;
|
|
798
|
+
explorerPanel.dispose();
|
|
799
|
+
});
|
|
800
|
+
it('should skip views already in correct area', async () => {
|
|
801
|
+
service.initialize();
|
|
802
|
+
const explorerWidget = new widgets_1.Widget();
|
|
803
|
+
explorerWidget.id = 'explorer';
|
|
804
|
+
const explorerPanel = new widgets_1.Panel();
|
|
805
|
+
widgets_1.Widget.attach(explorerPanel, document.body);
|
|
806
|
+
explorerPanel.addWidget(explorerWidget);
|
|
807
|
+
tryGetWidgetStub.withArgs('explorer').returns(explorerWidget);
|
|
808
|
+
getAreaForStub.withArgs(explorerWidget).returns('left');
|
|
809
|
+
const contribution = createMockViewContribution({
|
|
810
|
+
widgetId: 'explorer',
|
|
811
|
+
widgetName: 'Explorer',
|
|
812
|
+
defaultWidgetOptions: { area: 'left' }
|
|
813
|
+
});
|
|
814
|
+
service['appContributions'] = {
|
|
815
|
+
getContributions: () => [contribution]
|
|
816
|
+
};
|
|
817
|
+
addWidgetStub.resetHistory();
|
|
818
|
+
await service.resetCurrentPerspective();
|
|
819
|
+
(0, chai_1.expect)(addWidgetStub.called).to.be.false;
|
|
820
|
+
explorerPanel.dispose();
|
|
821
|
+
});
|
|
822
|
+
it('should skip views not attached', async () => {
|
|
823
|
+
service.initialize();
|
|
824
|
+
const explorerWidget = new widgets_1.Widget();
|
|
825
|
+
explorerWidget.id = 'explorer';
|
|
826
|
+
// Not attached to any parent
|
|
827
|
+
tryGetWidgetStub.withArgs('explorer').returns(explorerWidget);
|
|
828
|
+
const contribution = createMockViewContribution({
|
|
829
|
+
widgetId: 'explorer',
|
|
830
|
+
widgetName: 'Explorer',
|
|
831
|
+
defaultWidgetOptions: { area: 'left' }
|
|
832
|
+
});
|
|
833
|
+
service['appContributions'] = {
|
|
834
|
+
getContributions: () => [contribution]
|
|
835
|
+
};
|
|
836
|
+
addWidgetStub.resetHistory();
|
|
837
|
+
await service.resetCurrentPerspective();
|
|
838
|
+
// Phase 1 should not try to relocate (not attached)
|
|
839
|
+
// Phase 2 will call initializeLayout
|
|
840
|
+
(0, chai_1.expect)(addWidgetStub.called).to.be.false;
|
|
841
|
+
});
|
|
842
|
+
it('should call openView on AbstractViewContribution instances', async () => {
|
|
843
|
+
service.initialize();
|
|
844
|
+
const contribution = createMockViewContribution({
|
|
845
|
+
widgetId: 'explorer',
|
|
846
|
+
widgetName: 'Explorer',
|
|
847
|
+
defaultWidgetOptions: { area: 'left' }
|
|
848
|
+
});
|
|
849
|
+
service['appContributions'] = {
|
|
850
|
+
getContributions: () => [contribution]
|
|
851
|
+
};
|
|
852
|
+
await service.resetCurrentPerspective();
|
|
853
|
+
(0, chai_1.expect)(contribution.openView.calledOnce).to.be.true;
|
|
854
|
+
(0, chai_1.expect)(contribution.openView.calledWith({ activate: false })).to.be.true;
|
|
855
|
+
});
|
|
856
|
+
it('should NOT call openView on non-AbstractViewContribution contributions', async () => {
|
|
857
|
+
service.initialize();
|
|
858
|
+
const nonViewContrib = createMockNonViewContribution();
|
|
859
|
+
service['appContributions'] = {
|
|
860
|
+
getContributions: () => [nonViewContrib]
|
|
861
|
+
};
|
|
862
|
+
// Should not throw
|
|
863
|
+
await service.resetCurrentPerspective();
|
|
864
|
+
});
|
|
865
|
+
it('should handle combined scenario: open view in wrong area + closed view', async () => {
|
|
866
|
+
service.initialize();
|
|
867
|
+
// Explorer is open but in the wrong area
|
|
868
|
+
const explorerWidget = new widgets_1.Widget();
|
|
869
|
+
explorerWidget.id = 'explorer';
|
|
870
|
+
const panel = new widgets_1.Panel();
|
|
871
|
+
widgets_1.Widget.attach(panel, document.body);
|
|
872
|
+
panel.addWidget(explorerWidget);
|
|
873
|
+
tryGetWidgetStub.withArgs('explorer').returns(explorerWidget);
|
|
874
|
+
getAreaForStub.withArgs(explorerWidget).returns('right');
|
|
875
|
+
const explorerContrib = createMockViewContribution({
|
|
876
|
+
widgetId: 'explorer',
|
|
877
|
+
widgetName: 'Explorer',
|
|
878
|
+
defaultWidgetOptions: { area: 'left' }
|
|
879
|
+
});
|
|
880
|
+
// SCM is closed (tryGetWidget returns undefined)
|
|
881
|
+
tryGetWidgetStub.withArgs('scm').returns(undefined);
|
|
882
|
+
const scmContrib = createMockViewContribution({
|
|
883
|
+
widgetId: 'scm',
|
|
884
|
+
widgetName: 'SCM',
|
|
885
|
+
defaultWidgetOptions: { area: 'left' }
|
|
886
|
+
});
|
|
887
|
+
service['appContributions'] = {
|
|
888
|
+
getContributions: () => [explorerContrib, scmContrib]
|
|
889
|
+
};
|
|
890
|
+
addWidgetStub.resetHistory();
|
|
891
|
+
await service.resetCurrentPerspective();
|
|
892
|
+
// Phase 1: Explorer should be relocated
|
|
893
|
+
(0, chai_1.expect)(addWidgetStub.calledWith(explorerWidget, sinon.match({ area: 'left' }))).to.be.true;
|
|
894
|
+
// Phase 2: Both should have openView called
|
|
895
|
+
(0, chai_1.expect)(explorerContrib.openView.calledOnce).to.be.true;
|
|
896
|
+
(0, chai_1.expect)(scmContrib.openView.calledOnce).to.be.true;
|
|
897
|
+
panel.dispose();
|
|
898
|
+
});
|
|
899
|
+
it('should use viewContainerId as effectiveWidgetId when set', async () => {
|
|
900
|
+
service.initialize();
|
|
901
|
+
const containerWidget = new widgets_1.Widget();
|
|
902
|
+
containerWidget.id = 'explorer-container';
|
|
903
|
+
const panel = new widgets_1.Panel();
|
|
904
|
+
widgets_1.Widget.attach(panel, document.body);
|
|
905
|
+
panel.addWidget(containerWidget);
|
|
906
|
+
tryGetWidgetStub.withArgs('explorer-container').returns(containerWidget);
|
|
907
|
+
getAreaForStub.withArgs(containerWidget).returns('right');
|
|
908
|
+
const contribution = createMockViewContribution({
|
|
909
|
+
widgetId: 'explorer',
|
|
910
|
+
viewContainerId: 'explorer-container',
|
|
911
|
+
widgetName: 'Explorer',
|
|
912
|
+
defaultWidgetOptions: { area: 'left' }
|
|
913
|
+
});
|
|
914
|
+
service['appContributions'] = {
|
|
915
|
+
getContributions: () => [contribution]
|
|
916
|
+
};
|
|
917
|
+
addWidgetStub.resetHistory();
|
|
918
|
+
await service.resetCurrentPerspective();
|
|
919
|
+
(0, chai_1.expect)(tryGetWidgetStub.calledWith('explorer-container')).to.be.true;
|
|
920
|
+
(0, chai_1.expect)(addWidgetStub.calledWith(containerWidget, sinon.match({ area: 'left' }))).to.be.true;
|
|
921
|
+
panel.dispose();
|
|
922
|
+
});
|
|
923
|
+
it('should handle openView errors gracefully', async () => {
|
|
924
|
+
service.initialize();
|
|
925
|
+
const contribution = createMockViewContribution({
|
|
926
|
+
widgetId: 'broken',
|
|
927
|
+
widgetName: 'Broken',
|
|
928
|
+
defaultWidgetOptions: { area: 'left' }
|
|
929
|
+
});
|
|
930
|
+
contribution.openView.rejects(new Error('openView failed'));
|
|
931
|
+
service['appContributions'] = {
|
|
932
|
+
getContributions: () => [contribution]
|
|
933
|
+
};
|
|
934
|
+
// Should not throw
|
|
935
|
+
await service.resetCurrentPerspective();
|
|
936
|
+
(0, chai_1.expect)(mockLogger.warn.called).to.be.true;
|
|
937
|
+
});
|
|
938
|
+
it('should handle widget relocation errors gracefully', async () => {
|
|
939
|
+
service.initialize();
|
|
940
|
+
const explorerWidget = new widgets_1.Widget();
|
|
941
|
+
explorerWidget.id = 'explorer';
|
|
942
|
+
const panel = new widgets_1.Panel();
|
|
943
|
+
widgets_1.Widget.attach(panel, document.body);
|
|
944
|
+
panel.addWidget(explorerWidget);
|
|
945
|
+
tryGetWidgetStub.withArgs('explorer').returns(explorerWidget);
|
|
946
|
+
getAreaForStub.withArgs(explorerWidget).returns('right');
|
|
947
|
+
addWidgetStub.rejects(new Error('relocation failed'));
|
|
948
|
+
const contribution = createMockViewContribution({
|
|
949
|
+
widgetId: 'explorer',
|
|
950
|
+
widgetName: 'Explorer',
|
|
951
|
+
defaultWidgetOptions: { area: 'left' }
|
|
952
|
+
});
|
|
953
|
+
service['appContributions'] = {
|
|
954
|
+
getContributions: () => [contribution]
|
|
955
|
+
};
|
|
956
|
+
// Should not throw
|
|
957
|
+
await service.resetCurrentPerspective();
|
|
958
|
+
(0, chai_1.expect)(mockLogger.warn.called).to.be.true;
|
|
959
|
+
panel.dispose();
|
|
960
|
+
});
|
|
961
|
+
it('should fire onDidChangePerspective event on default perspective reset', async () => {
|
|
962
|
+
service.initialize();
|
|
963
|
+
service['appContributions'] = {
|
|
964
|
+
getContributions: () => []
|
|
965
|
+
};
|
|
966
|
+
const spy = sinon.spy();
|
|
967
|
+
service.onDidChangePerspective(spy);
|
|
968
|
+
await service.resetCurrentPerspective();
|
|
969
|
+
(0, chai_1.expect)(spy.calledOnce).to.be.true;
|
|
970
|
+
(0, chai_1.expect)(spy.calledWith(perspective_service_1.PerspectiveServiceImpl.DEFAULT_PERSPECTIVE_ID)).to.be.true;
|
|
971
|
+
});
|
|
972
|
+
it('should skip contributions without defaultViewOptions.area in Phase 1', async () => {
|
|
973
|
+
service.initialize();
|
|
974
|
+
const contribution = createMockViewContribution({
|
|
975
|
+
widgetId: 'no-area',
|
|
976
|
+
widgetName: 'No Area',
|
|
977
|
+
defaultWidgetOptions: {}
|
|
978
|
+
});
|
|
979
|
+
service['appContributions'] = {
|
|
980
|
+
getContributions: () => [contribution]
|
|
981
|
+
};
|
|
982
|
+
addWidgetStub.resetHistory();
|
|
983
|
+
await service.resetCurrentPerspective();
|
|
984
|
+
// Phase 1 should skip (no target area)
|
|
985
|
+
// Phase 2 should still call openView
|
|
986
|
+
(0, chai_1.expect)(contribution.openView.calledOnce).to.be.true;
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
describe('layout healing on perspective switch', () => {
|
|
990
|
+
it('should replace disposed side panel widget when switching back to a perspective', async () => {
|
|
991
|
+
service.registerPerspective({
|
|
992
|
+
id: 'perspA',
|
|
993
|
+
label: 'A',
|
|
994
|
+
viewPlacements: new Map()
|
|
995
|
+
});
|
|
996
|
+
service.registerPerspective({
|
|
997
|
+
id: 'perspB',
|
|
998
|
+
label: 'B',
|
|
999
|
+
viewPlacements: new Map()
|
|
1000
|
+
});
|
|
1001
|
+
// Create a widget and then dispose it to simulate cross-perspective closure
|
|
1002
|
+
const disposedWidget = new widgets_1.Widget();
|
|
1003
|
+
disposedWidget.id = 'explorer-widget';
|
|
1004
|
+
disposedWidget.dispose();
|
|
1005
|
+
(0, chai_1.expect)(disposedWidget.isDisposed).to.be.true;
|
|
1006
|
+
const freshWidget = new widgets_1.Widget();
|
|
1007
|
+
freshWidget.id = 'explorer-widget';
|
|
1008
|
+
getOrCreateWidgetStub.withArgs('explorer-widget').resolves(freshWidget);
|
|
1009
|
+
const savedLayout = {
|
|
1010
|
+
leftPanel: {
|
|
1011
|
+
type: 'sidepanel',
|
|
1012
|
+
items: [{ widget: disposedWidget, rank: 0, expanded: true }]
|
|
1013
|
+
},
|
|
1014
|
+
mainPanel: undefined,
|
|
1015
|
+
bottomPanel: undefined
|
|
1016
|
+
};
|
|
1017
|
+
// Switch to A, then to B
|
|
1018
|
+
await service.switchPerspective('perspA');
|
|
1019
|
+
await service.switchPerspective('perspB');
|
|
1020
|
+
// Manually set the saved layout for A with the disposed widget
|
|
1021
|
+
service.setSavedLayout('perspA', savedLayout);
|
|
1022
|
+
setLayoutDataStub.resetHistory();
|
|
1023
|
+
getOrCreateWidgetStub.resetHistory();
|
|
1024
|
+
// Switch back to A
|
|
1025
|
+
await service.switchPerspective('perspA');
|
|
1026
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('explorer-widget')).to.be.true;
|
|
1027
|
+
(0, chai_1.expect)(setLayoutDataStub.calledOnce).to.be.true;
|
|
1028
|
+
// The layout should now contain the fresh widget
|
|
1029
|
+
const restoredLayout = setLayoutDataStub.firstCall.args[0];
|
|
1030
|
+
(0, chai_1.expect)(restoredLayout.leftPanel.items[0].widget).to.equal(freshWidget);
|
|
1031
|
+
});
|
|
1032
|
+
it('should replace disposed dock panel widget (main area)', async () => {
|
|
1033
|
+
service.registerPerspective({
|
|
1034
|
+
id: 'perspA',
|
|
1035
|
+
label: 'A',
|
|
1036
|
+
viewPlacements: new Map()
|
|
1037
|
+
});
|
|
1038
|
+
service.registerPerspective({
|
|
1039
|
+
id: 'perspB',
|
|
1040
|
+
label: 'B',
|
|
1041
|
+
viewPlacements: new Map()
|
|
1042
|
+
});
|
|
1043
|
+
const disposedWidget = new widgets_1.Widget();
|
|
1044
|
+
disposedWidget.id = 'editor-widget';
|
|
1045
|
+
disposedWidget.dispose();
|
|
1046
|
+
const freshWidget = new widgets_1.Widget();
|
|
1047
|
+
freshWidget.id = 'editor-widget';
|
|
1048
|
+
getOrCreateWidgetStub.withArgs('editor-widget').resolves(freshWidget);
|
|
1049
|
+
const savedLayout = {
|
|
1050
|
+
mainPanel: {
|
|
1051
|
+
main: {
|
|
1052
|
+
type: 'tab-area',
|
|
1053
|
+
widgets: [disposedWidget],
|
|
1054
|
+
currentIndex: 0
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
bottomPanel: undefined
|
|
1058
|
+
};
|
|
1059
|
+
await service.switchPerspective('perspA');
|
|
1060
|
+
await service.switchPerspective('perspB');
|
|
1061
|
+
service.setSavedLayout('perspA', savedLayout);
|
|
1062
|
+
setLayoutDataStub.resetHistory();
|
|
1063
|
+
getOrCreateWidgetStub.resetHistory();
|
|
1064
|
+
await service.switchPerspective('perspA');
|
|
1065
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('editor-widget')).to.be.true;
|
|
1066
|
+
const restoredLayout = setLayoutDataStub.firstCall.args[0];
|
|
1067
|
+
(0, chai_1.expect)(restoredLayout.mainPanel.main.widgets[0]).to.equal(freshWidget);
|
|
1068
|
+
});
|
|
1069
|
+
it('should handle recreation failure gracefully', async () => {
|
|
1070
|
+
service.registerPerspective({
|
|
1071
|
+
id: 'perspA',
|
|
1072
|
+
label: 'A',
|
|
1073
|
+
viewPlacements: new Map()
|
|
1074
|
+
});
|
|
1075
|
+
service.registerPerspective({
|
|
1076
|
+
id: 'perspB',
|
|
1077
|
+
label: 'B',
|
|
1078
|
+
viewPlacements: new Map()
|
|
1079
|
+
});
|
|
1080
|
+
const disposedWidget = new widgets_1.Widget();
|
|
1081
|
+
disposedWidget.id = 'broken-widget';
|
|
1082
|
+
disposedWidget.dispose();
|
|
1083
|
+
getOrCreateWidgetStub.withArgs('broken-widget').rejects(new Error('No factory'));
|
|
1084
|
+
const savedLayout = {
|
|
1085
|
+
leftPanel: {
|
|
1086
|
+
type: 'sidepanel',
|
|
1087
|
+
items: [{ widget: disposedWidget, rank: 0, expanded: true }]
|
|
1088
|
+
},
|
|
1089
|
+
mainPanel: undefined,
|
|
1090
|
+
bottomPanel: undefined
|
|
1091
|
+
};
|
|
1092
|
+
await service.switchPerspective('perspA');
|
|
1093
|
+
await service.switchPerspective('perspB');
|
|
1094
|
+
service.setSavedLayout('perspA', savedLayout);
|
|
1095
|
+
mockLogger.warn.resetHistory();
|
|
1096
|
+
setLayoutDataStub.resetHistory();
|
|
1097
|
+
await service.switchPerspective('perspA');
|
|
1098
|
+
// Widget should be set to undefined in the layout
|
|
1099
|
+
const restoredLayout = setLayoutDataStub.firstCall.args[0];
|
|
1100
|
+
(0, chai_1.expect)(restoredLayout.leftPanel.items[0].widget).to.be.undefined;
|
|
1101
|
+
// Warn log should have been called
|
|
1102
|
+
(0, chai_1.expect)(mockLogger.warn.called).to.be.true;
|
|
1103
|
+
const healCall = mockLogger.warn.getCalls().find((c) => c.args[0] === 'Failed to recreate disposed widget for perspective layout');
|
|
1104
|
+
(0, chai_1.expect)(healCall).to.not.be.undefined;
|
|
1105
|
+
// setLayoutData should still be called
|
|
1106
|
+
(0, chai_1.expect)(setLayoutDataStub.calledOnce).to.be.true;
|
|
1107
|
+
});
|
|
1108
|
+
it('should handle recreation failure in dock panel by splicing widget', async () => {
|
|
1109
|
+
service.registerPerspective({
|
|
1110
|
+
id: 'perspA',
|
|
1111
|
+
label: 'A',
|
|
1112
|
+
viewPlacements: new Map()
|
|
1113
|
+
});
|
|
1114
|
+
service.registerPerspective({
|
|
1115
|
+
id: 'perspB',
|
|
1116
|
+
label: 'B',
|
|
1117
|
+
viewPlacements: new Map()
|
|
1118
|
+
});
|
|
1119
|
+
const disposedWidget = new widgets_1.Widget();
|
|
1120
|
+
disposedWidget.id = 'broken-widget';
|
|
1121
|
+
disposedWidget.dispose();
|
|
1122
|
+
const goodWidget = new widgets_1.Widget();
|
|
1123
|
+
goodWidget.id = 'good-widget';
|
|
1124
|
+
getOrCreateWidgetStub.withArgs('broken-widget').rejects(new Error('No factory'));
|
|
1125
|
+
const savedLayout = {
|
|
1126
|
+
mainPanel: {
|
|
1127
|
+
main: {
|
|
1128
|
+
type: 'tab-area',
|
|
1129
|
+
widgets: [goodWidget, disposedWidget],
|
|
1130
|
+
currentIndex: 0
|
|
1131
|
+
}
|
|
1132
|
+
},
|
|
1133
|
+
bottomPanel: undefined
|
|
1134
|
+
};
|
|
1135
|
+
await service.switchPerspective('perspA');
|
|
1136
|
+
await service.switchPerspective('perspB');
|
|
1137
|
+
service.setSavedLayout('perspA', savedLayout);
|
|
1138
|
+
setLayoutDataStub.resetHistory();
|
|
1139
|
+
await service.switchPerspective('perspA');
|
|
1140
|
+
const restoredLayout = setLayoutDataStub.firstCall.args[0];
|
|
1141
|
+
const tabArea = restoredLayout.mainPanel.main;
|
|
1142
|
+
// The broken widget should have been spliced out
|
|
1143
|
+
(0, chai_1.expect)(tabArea.widgets).to.have.lengthOf(1);
|
|
1144
|
+
(0, chai_1.expect)(tabArea.widgets[0]).to.equal(goodWidget);
|
|
1145
|
+
});
|
|
1146
|
+
it('should not touch non-disposed widgets', async () => {
|
|
1147
|
+
service.registerPerspective({
|
|
1148
|
+
id: 'perspA',
|
|
1149
|
+
label: 'A',
|
|
1150
|
+
viewPlacements: new Map()
|
|
1151
|
+
});
|
|
1152
|
+
service.registerPerspective({
|
|
1153
|
+
id: 'perspB',
|
|
1154
|
+
label: 'B',
|
|
1155
|
+
viewPlacements: new Map()
|
|
1156
|
+
});
|
|
1157
|
+
const liveWidget = new widgets_1.Widget();
|
|
1158
|
+
liveWidget.id = 'live-widget';
|
|
1159
|
+
const disposedWidget = new widgets_1.Widget();
|
|
1160
|
+
disposedWidget.id = 'dead-widget';
|
|
1161
|
+
disposedWidget.dispose();
|
|
1162
|
+
const freshWidget = new widgets_1.Widget();
|
|
1163
|
+
freshWidget.id = 'dead-widget';
|
|
1164
|
+
getOrCreateWidgetStub.withArgs('dead-widget').resolves(freshWidget);
|
|
1165
|
+
const savedLayout = {
|
|
1166
|
+
leftPanel: {
|
|
1167
|
+
type: 'sidepanel',
|
|
1168
|
+
items: [
|
|
1169
|
+
{ widget: liveWidget, rank: 0, expanded: true },
|
|
1170
|
+
{ widget: disposedWidget, rank: 1, expanded: false }
|
|
1171
|
+
]
|
|
1172
|
+
},
|
|
1173
|
+
mainPanel: undefined,
|
|
1174
|
+
bottomPanel: undefined
|
|
1175
|
+
};
|
|
1176
|
+
await service.switchPerspective('perspA');
|
|
1177
|
+
await service.switchPerspective('perspB');
|
|
1178
|
+
service.setSavedLayout('perspA', savedLayout);
|
|
1179
|
+
getOrCreateWidgetStub.resetHistory();
|
|
1180
|
+
setLayoutDataStub.resetHistory();
|
|
1181
|
+
await service.switchPerspective('perspA');
|
|
1182
|
+
// Only the disposed widget should trigger getOrCreateWidget
|
|
1183
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledOnce).to.be.true;
|
|
1184
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('dead-widget')).to.be.true;
|
|
1185
|
+
// The live widget should remain unchanged
|
|
1186
|
+
const restoredLayout = setLayoutDataStub.firstCall.args[0];
|
|
1187
|
+
(0, chai_1.expect)(restoredLayout.leftPanel.items[0].widget).to.equal(liveWidget);
|
|
1188
|
+
(0, chai_1.expect)(restoredLayout.leftPanel.items[1].widget).to.equal(freshWidget);
|
|
1189
|
+
});
|
|
1190
|
+
it('should handle nested split-area configs', async () => {
|
|
1191
|
+
service.registerPerspective({
|
|
1192
|
+
id: 'perspA',
|
|
1193
|
+
label: 'A',
|
|
1194
|
+
viewPlacements: new Map()
|
|
1195
|
+
});
|
|
1196
|
+
service.registerPerspective({
|
|
1197
|
+
id: 'perspB',
|
|
1198
|
+
label: 'B',
|
|
1199
|
+
viewPlacements: new Map()
|
|
1200
|
+
});
|
|
1201
|
+
const disposedWidget = new widgets_1.Widget();
|
|
1202
|
+
disposedWidget.id = 'nested-widget';
|
|
1203
|
+
disposedWidget.dispose();
|
|
1204
|
+
const freshWidget = new widgets_1.Widget();
|
|
1205
|
+
freshWidget.id = 'nested-widget';
|
|
1206
|
+
getOrCreateWidgetStub.withArgs('nested-widget').resolves(freshWidget);
|
|
1207
|
+
const liveWidget = new widgets_1.Widget();
|
|
1208
|
+
liveWidget.id = 'other-widget';
|
|
1209
|
+
const savedLayout = {
|
|
1210
|
+
mainPanel: {
|
|
1211
|
+
main: {
|
|
1212
|
+
type: 'split-area',
|
|
1213
|
+
orientation: 'horizontal',
|
|
1214
|
+
sizes: [0.5, 0.5],
|
|
1215
|
+
children: [
|
|
1216
|
+
{
|
|
1217
|
+
type: 'tab-area',
|
|
1218
|
+
widgets: [liveWidget],
|
|
1219
|
+
currentIndex: 0
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
type: 'tab-area',
|
|
1223
|
+
widgets: [disposedWidget],
|
|
1224
|
+
currentIndex: 0
|
|
1225
|
+
}
|
|
1226
|
+
]
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
bottomPanel: undefined
|
|
1230
|
+
};
|
|
1231
|
+
await service.switchPerspective('perspA');
|
|
1232
|
+
await service.switchPerspective('perspB');
|
|
1233
|
+
service.setSavedLayout('perspA', savedLayout);
|
|
1234
|
+
getOrCreateWidgetStub.resetHistory();
|
|
1235
|
+
setLayoutDataStub.resetHistory();
|
|
1236
|
+
await service.switchPerspective('perspA');
|
|
1237
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('nested-widget')).to.be.true;
|
|
1238
|
+
const restoredLayout = setLayoutDataStub.firstCall.args[0];
|
|
1239
|
+
const splitArea = restoredLayout.mainPanel.main;
|
|
1240
|
+
(0, chai_1.expect)(splitArea.children[0].widgets[0]).to.equal(liveWidget);
|
|
1241
|
+
(0, chai_1.expect)(splitArea.children[1].widgets[0]).to.equal(freshWidget);
|
|
1242
|
+
});
|
|
1243
|
+
it('should handle empty/missing panel sections', async () => {
|
|
1244
|
+
service.registerPerspective({
|
|
1245
|
+
id: 'perspA',
|
|
1246
|
+
label: 'A',
|
|
1247
|
+
viewPlacements: new Map()
|
|
1248
|
+
});
|
|
1249
|
+
service.registerPerspective({
|
|
1250
|
+
id: 'perspB',
|
|
1251
|
+
label: 'B',
|
|
1252
|
+
viewPlacements: new Map()
|
|
1253
|
+
});
|
|
1254
|
+
const savedLayout = {
|
|
1255
|
+
mainPanel: undefined,
|
|
1256
|
+
bottomPanel: undefined,
|
|
1257
|
+
leftPanel: undefined,
|
|
1258
|
+
rightPanel: undefined
|
|
1259
|
+
};
|
|
1260
|
+
await service.switchPerspective('perspA');
|
|
1261
|
+
await service.switchPerspective('perspB');
|
|
1262
|
+
service.setSavedLayout('perspA', savedLayout);
|
|
1263
|
+
setLayoutDataStub.resetHistory();
|
|
1264
|
+
// Should not throw
|
|
1265
|
+
await service.switchPerspective('perspA');
|
|
1266
|
+
(0, chai_1.expect)(setLayoutDataStub.calledOnce).to.be.true;
|
|
1267
|
+
});
|
|
1268
|
+
it('should heal bottom panel disposed widgets', async () => {
|
|
1269
|
+
service.registerPerspective({
|
|
1270
|
+
id: 'perspA',
|
|
1271
|
+
label: 'A',
|
|
1272
|
+
viewPlacements: new Map()
|
|
1273
|
+
});
|
|
1274
|
+
service.registerPerspective({
|
|
1275
|
+
id: 'perspB',
|
|
1276
|
+
label: 'B',
|
|
1277
|
+
viewPlacements: new Map()
|
|
1278
|
+
});
|
|
1279
|
+
const disposedWidget = new widgets_1.Widget();
|
|
1280
|
+
disposedWidget.id = 'bottom-widget';
|
|
1281
|
+
disposedWidget.dispose();
|
|
1282
|
+
const freshWidget = new widgets_1.Widget();
|
|
1283
|
+
freshWidget.id = 'bottom-widget';
|
|
1284
|
+
getOrCreateWidgetStub.withArgs('bottom-widget').resolves(freshWidget);
|
|
1285
|
+
const savedLayout = {
|
|
1286
|
+
mainPanel: undefined,
|
|
1287
|
+
bottomPanel: {
|
|
1288
|
+
config: {
|
|
1289
|
+
main: {
|
|
1290
|
+
type: 'tab-area',
|
|
1291
|
+
widgets: [disposedWidget],
|
|
1292
|
+
currentIndex: 0
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
await service.switchPerspective('perspA');
|
|
1298
|
+
await service.switchPerspective('perspB');
|
|
1299
|
+
service.setSavedLayout('perspA', savedLayout);
|
|
1300
|
+
getOrCreateWidgetStub.resetHistory();
|
|
1301
|
+
setLayoutDataStub.resetHistory();
|
|
1302
|
+
await service.switchPerspective('perspA');
|
|
1303
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('bottom-widget')).to.be.true;
|
|
1304
|
+
const restoredLayout = setLayoutDataStub.firstCall.args[0];
|
|
1305
|
+
(0, chai_1.expect)(restoredLayout.bottomPanel.config.main.widgets[0]).to.equal(freshWidget);
|
|
1306
|
+
});
|
|
1307
|
+
it('should heal right panel disposed widgets', async () => {
|
|
1308
|
+
service.registerPerspective({
|
|
1309
|
+
id: 'perspA',
|
|
1310
|
+
label: 'A',
|
|
1311
|
+
viewPlacements: new Map()
|
|
1312
|
+
});
|
|
1313
|
+
service.registerPerspective({
|
|
1314
|
+
id: 'perspB',
|
|
1315
|
+
label: 'B',
|
|
1316
|
+
viewPlacements: new Map()
|
|
1317
|
+
});
|
|
1318
|
+
const disposedWidget = new widgets_1.Widget();
|
|
1319
|
+
disposedWidget.id = 'right-widget';
|
|
1320
|
+
disposedWidget.dispose();
|
|
1321
|
+
const freshWidget = new widgets_1.Widget();
|
|
1322
|
+
freshWidget.id = 'right-widget';
|
|
1323
|
+
getOrCreateWidgetStub.withArgs('right-widget').resolves(freshWidget);
|
|
1324
|
+
const savedLayout = {
|
|
1325
|
+
mainPanel: undefined,
|
|
1326
|
+
bottomPanel: undefined,
|
|
1327
|
+
rightPanel: {
|
|
1328
|
+
type: 'sidepanel',
|
|
1329
|
+
items: [{ widget: disposedWidget, rank: 0, expanded: true }]
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
await service.switchPerspective('perspA');
|
|
1333
|
+
await service.switchPerspective('perspB');
|
|
1334
|
+
service.setSavedLayout('perspA', savedLayout);
|
|
1335
|
+
getOrCreateWidgetStub.resetHistory();
|
|
1336
|
+
setLayoutDataStub.resetHistory();
|
|
1337
|
+
await service.switchPerspective('perspA');
|
|
1338
|
+
(0, chai_1.expect)(getOrCreateWidgetStub.calledWith('right-widget')).to.be.true;
|
|
1339
|
+
const restoredLayout = setLayoutDataStub.firstCall.args[0];
|
|
1340
|
+
(0, chai_1.expect)(restoredLayout.rightPanel.items[0].widget).to.equal(freshWidget);
|
|
1341
|
+
});
|
|
1342
|
+
});
|
|
1343
|
+
describe('Plumbing API (layout persistence)', () => {
|
|
1344
|
+
it('should expose defaultPerspectiveId matching the static constant', () => {
|
|
1345
|
+
(0, chai_1.expect)(service.defaultPerspectiveId).to.equal(perspective_service_1.PerspectiveServiceImpl.DEFAULT_PERSPECTIVE_ID);
|
|
1346
|
+
});
|
|
1347
|
+
it('should return default perspective ID when none is set', () => {
|
|
1348
|
+
(0, chai_1.expect)(service.getActivePerspectiveId()).to.equal(perspective_service_1.PerspectiveServiceImpl.DEFAULT_PERSPECTIVE_ID);
|
|
1349
|
+
});
|
|
1350
|
+
it('should return active perspective ID after switching', async () => {
|
|
1351
|
+
service.registerPerspective({
|
|
1352
|
+
id: 'test-persp',
|
|
1353
|
+
label: 'Test',
|
|
1354
|
+
viewPlacements: new Map()
|
|
1355
|
+
});
|
|
1356
|
+
await service.switchPerspective('test-persp');
|
|
1357
|
+
(0, chai_1.expect)(service.getActivePerspectiveId()).to.equal('test-persp');
|
|
1358
|
+
});
|
|
1359
|
+
it('should round-trip getSavedLayout/setSavedLayout', () => {
|
|
1360
|
+
const layout = { mainPanel: { widgets: ['w1'] }, bottomPanel: {} };
|
|
1361
|
+
service.setSavedLayout('persp-x', layout);
|
|
1362
|
+
(0, chai_1.expect)(service.getSavedLayout('persp-x')).to.equal(layout);
|
|
1363
|
+
});
|
|
1364
|
+
it('should return undefined for non-existent saved layout', () => {
|
|
1365
|
+
(0, chai_1.expect)(service.getSavedLayout('no-such-persp')).to.be.undefined;
|
|
1366
|
+
});
|
|
1367
|
+
it('should return correct saved perspective IDs', () => {
|
|
1368
|
+
const layout1 = { mainPanel: {} };
|
|
1369
|
+
const layout2 = { mainPanel: {} };
|
|
1370
|
+
service.setSavedLayout('a', layout1);
|
|
1371
|
+
service.setSavedLayout('b', layout2);
|
|
1372
|
+
const ids = service.getSavedPerspectiveIds();
|
|
1373
|
+
(0, chai_1.expect)(ids).to.include('a');
|
|
1374
|
+
(0, chai_1.expect)(ids).to.include('b');
|
|
1375
|
+
(0, chai_1.expect)(ids).to.have.lengthOf(2);
|
|
1376
|
+
});
|
|
1377
|
+
it('should set active perspective ID only for registered perspectives', () => {
|
|
1378
|
+
service.registerPerspective({
|
|
1379
|
+
id: 'registered',
|
|
1380
|
+
label: 'Registered',
|
|
1381
|
+
viewPlacements: new Map()
|
|
1382
|
+
});
|
|
1383
|
+
(0, chai_1.expect)(service.setActivePerspectiveId('registered')).to.be.true;
|
|
1384
|
+
(0, chai_1.expect)(service.getActivePerspectiveId()).to.equal('registered');
|
|
1385
|
+
(0, chai_1.expect)(service.setActivePerspectiveId('unregistered')).to.be.false;
|
|
1386
|
+
(0, chai_1.expect)(service.getActivePerspectiveId()).to.equal('registered');
|
|
1387
|
+
});
|
|
1388
|
+
it('should clear all saved layouts', () => {
|
|
1389
|
+
const layout = { mainPanel: {} };
|
|
1390
|
+
service.setSavedLayout('a', layout);
|
|
1391
|
+
service.setSavedLayout('b', layout);
|
|
1392
|
+
service.clearSavedLayouts();
|
|
1393
|
+
(0, chai_1.expect)(service.getSavedPerspectiveIds()).to.have.lengthOf(0);
|
|
1394
|
+
(0, chai_1.expect)(service.getSavedLayout('a')).to.be.undefined;
|
|
1395
|
+
(0, chai_1.expect)(service.getSavedLayout('b')).to.be.undefined;
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
describe('collectWidgetIds', () => {
|
|
1399
|
+
it('should collect IDs from side panel items', () => {
|
|
1400
|
+
const widgetA = new widgets_1.Widget();
|
|
1401
|
+
widgetA.id = 'widget-a';
|
|
1402
|
+
const widgetB = new widgets_1.Widget();
|
|
1403
|
+
widgetB.id = 'widget-b';
|
|
1404
|
+
const layout = {
|
|
1405
|
+
leftPanel: {
|
|
1406
|
+
type: 'sidepanel',
|
|
1407
|
+
items: [
|
|
1408
|
+
{ widget: widgetA, rank: 0, expanded: true },
|
|
1409
|
+
{ widget: widgetB, rank: 1, expanded: false }
|
|
1410
|
+
]
|
|
1411
|
+
},
|
|
1412
|
+
mainPanel: undefined,
|
|
1413
|
+
bottomPanel: undefined
|
|
1414
|
+
};
|
|
1415
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1416
|
+
const ids = service.collectWidgetIds(layout);
|
|
1417
|
+
(0, chai_1.expect)(ids.has('widget-a')).to.be.true;
|
|
1418
|
+
(0, chai_1.expect)(ids.has('widget-b')).to.be.true;
|
|
1419
|
+
(0, chai_1.expect)(ids.size).to.equal(2);
|
|
1420
|
+
});
|
|
1421
|
+
it('should collect IDs from dock panel tab-areas', () => {
|
|
1422
|
+
const widgetA = new widgets_1.Widget();
|
|
1423
|
+
widgetA.id = 'tab-widget-a';
|
|
1424
|
+
const widgetB = new widgets_1.Widget();
|
|
1425
|
+
widgetB.id = 'tab-widget-b';
|
|
1426
|
+
const layout = {
|
|
1427
|
+
mainPanel: {
|
|
1428
|
+
main: {
|
|
1429
|
+
type: 'tab-area',
|
|
1430
|
+
widgets: [widgetA, widgetB],
|
|
1431
|
+
currentIndex: 0
|
|
1432
|
+
}
|
|
1433
|
+
},
|
|
1434
|
+
bottomPanel: undefined
|
|
1435
|
+
};
|
|
1436
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1437
|
+
const ids = service.collectWidgetIds(layout);
|
|
1438
|
+
(0, chai_1.expect)(ids.has('tab-widget-a')).to.be.true;
|
|
1439
|
+
(0, chai_1.expect)(ids.has('tab-widget-b')).to.be.true;
|
|
1440
|
+
(0, chai_1.expect)(ids.size).to.equal(2);
|
|
1441
|
+
});
|
|
1442
|
+
it('should collect IDs from nested split-areas', () => {
|
|
1443
|
+
const widgetA = new widgets_1.Widget();
|
|
1444
|
+
widgetA.id = 'split-a';
|
|
1445
|
+
const widgetB = new widgets_1.Widget();
|
|
1446
|
+
widgetB.id = 'split-b';
|
|
1447
|
+
const layout = {
|
|
1448
|
+
mainPanel: {
|
|
1449
|
+
main: {
|
|
1450
|
+
type: 'split-area',
|
|
1451
|
+
orientation: 'horizontal',
|
|
1452
|
+
sizes: [0.5, 0.5],
|
|
1453
|
+
children: [
|
|
1454
|
+
{
|
|
1455
|
+
type: 'tab-area',
|
|
1456
|
+
widgets: [widgetA],
|
|
1457
|
+
currentIndex: 0
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
type: 'tab-area',
|
|
1461
|
+
widgets: [widgetB],
|
|
1462
|
+
currentIndex: 0
|
|
1463
|
+
}
|
|
1464
|
+
]
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
bottomPanel: undefined
|
|
1468
|
+
};
|
|
1469
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1470
|
+
const ids = service.collectWidgetIds(layout);
|
|
1471
|
+
(0, chai_1.expect)(ids.has('split-a')).to.be.true;
|
|
1472
|
+
(0, chai_1.expect)(ids.has('split-b')).to.be.true;
|
|
1473
|
+
(0, chai_1.expect)(ids.size).to.equal(2);
|
|
1474
|
+
});
|
|
1475
|
+
it('should return empty set for empty/missing layout sections', () => {
|
|
1476
|
+
const layout = {
|
|
1477
|
+
mainPanel: undefined,
|
|
1478
|
+
bottomPanel: undefined,
|
|
1479
|
+
leftPanel: undefined,
|
|
1480
|
+
rightPanel: undefined
|
|
1481
|
+
};
|
|
1482
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1483
|
+
const ids = service.collectWidgetIds(layout);
|
|
1484
|
+
(0, chai_1.expect)(ids.size).to.equal(0);
|
|
1485
|
+
});
|
|
1486
|
+
it('should skip undefined widget entries in tab-areas', () => {
|
|
1487
|
+
const widgetA = new widgets_1.Widget();
|
|
1488
|
+
widgetA.id = 'valid-widget';
|
|
1489
|
+
const layout = {
|
|
1490
|
+
mainPanel: {
|
|
1491
|
+
main: {
|
|
1492
|
+
type: 'tab-area',
|
|
1493
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1494
|
+
widgets: [widgetA, undefined],
|
|
1495
|
+
currentIndex: 0
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
1498
|
+
bottomPanel: undefined
|
|
1499
|
+
};
|
|
1500
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1501
|
+
const ids = service.collectWidgetIds(layout);
|
|
1502
|
+
(0, chai_1.expect)(ids.has('valid-widget')).to.be.true;
|
|
1503
|
+
(0, chai_1.expect)(ids.size).to.equal(1);
|
|
1504
|
+
});
|
|
1505
|
+
it('should skip undefined widget entries in side panel items', () => {
|
|
1506
|
+
const widgetA = new widgets_1.Widget();
|
|
1507
|
+
widgetA.id = 'valid-side-widget';
|
|
1508
|
+
const layout = {
|
|
1509
|
+
leftPanel: {
|
|
1510
|
+
type: 'sidepanel',
|
|
1511
|
+
items: [
|
|
1512
|
+
{ widget: widgetA, rank: 0, expanded: true },
|
|
1513
|
+
{ widget: undefined, rank: 1, expanded: false }
|
|
1514
|
+
]
|
|
1515
|
+
},
|
|
1516
|
+
mainPanel: undefined,
|
|
1517
|
+
bottomPanel: undefined
|
|
1518
|
+
};
|
|
1519
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1520
|
+
const ids = service.collectWidgetIds(layout);
|
|
1521
|
+
(0, chai_1.expect)(ids.has('valid-side-widget')).to.be.true;
|
|
1522
|
+
(0, chai_1.expect)(ids.size).to.equal(1);
|
|
1523
|
+
});
|
|
1524
|
+
it('should collect IDs from bottom panel', () => {
|
|
1525
|
+
const widgetA = new widgets_1.Widget();
|
|
1526
|
+
widgetA.id = 'bottom-widget';
|
|
1527
|
+
const layout = {
|
|
1528
|
+
mainPanel: undefined,
|
|
1529
|
+
bottomPanel: {
|
|
1530
|
+
config: {
|
|
1531
|
+
main: {
|
|
1532
|
+
type: 'tab-area',
|
|
1533
|
+
widgets: [widgetA],
|
|
1534
|
+
currentIndex: 0
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1540
|
+
const ids = service.collectWidgetIds(layout);
|
|
1541
|
+
(0, chai_1.expect)(ids.has('bottom-widget')).to.be.true;
|
|
1542
|
+
(0, chai_1.expect)(ids.size).to.equal(1);
|
|
1543
|
+
});
|
|
1544
|
+
it('should collect IDs from all panels', () => {
|
|
1545
|
+
const leftWidget = new widgets_1.Widget();
|
|
1546
|
+
leftWidget.id = 'left-w';
|
|
1547
|
+
const rightWidget = new widgets_1.Widget();
|
|
1548
|
+
rightWidget.id = 'right-w';
|
|
1549
|
+
const mainWidget = new widgets_1.Widget();
|
|
1550
|
+
mainWidget.id = 'main-w';
|
|
1551
|
+
const bottomWidget = new widgets_1.Widget();
|
|
1552
|
+
bottomWidget.id = 'bottom-w';
|
|
1553
|
+
const layout = {
|
|
1554
|
+
leftPanel: {
|
|
1555
|
+
type: 'sidepanel',
|
|
1556
|
+
items: [{ widget: leftWidget, rank: 0, expanded: true }]
|
|
1557
|
+
},
|
|
1558
|
+
rightPanel: {
|
|
1559
|
+
type: 'sidepanel',
|
|
1560
|
+
items: [{ widget: rightWidget, rank: 0, expanded: true }]
|
|
1561
|
+
},
|
|
1562
|
+
mainPanel: {
|
|
1563
|
+
main: {
|
|
1564
|
+
type: 'tab-area',
|
|
1565
|
+
widgets: [mainWidget],
|
|
1566
|
+
currentIndex: 0
|
|
1567
|
+
}
|
|
1568
|
+
},
|
|
1569
|
+
bottomPanel: {
|
|
1570
|
+
config: {
|
|
1571
|
+
main: {
|
|
1572
|
+
type: 'tab-area',
|
|
1573
|
+
widgets: [bottomWidget],
|
|
1574
|
+
currentIndex: 0
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1580
|
+
const ids = service.collectWidgetIds(layout);
|
|
1581
|
+
(0, chai_1.expect)(ids.has('left-w')).to.be.true;
|
|
1582
|
+
(0, chai_1.expect)(ids.has('right-w')).to.be.true;
|
|
1583
|
+
(0, chai_1.expect)(ids.has('main-w')).to.be.true;
|
|
1584
|
+
(0, chai_1.expect)(ids.has('bottom-w')).to.be.true;
|
|
1585
|
+
(0, chai_1.expect)(ids.size).to.equal(4);
|
|
1586
|
+
});
|
|
1587
|
+
});
|
|
1588
|
+
describe('detachStrayWidgets', () => {
|
|
1589
|
+
it('should detach a left-panel widget whose ID is not in the layout ID set', () => {
|
|
1590
|
+
const strayWidget = new widgets_1.Widget();
|
|
1591
|
+
strayWidget.id = 'stray-left';
|
|
1592
|
+
const panel = new widgets_1.Panel();
|
|
1593
|
+
widgets_1.Widget.attach(panel, document.body);
|
|
1594
|
+
panel.addWidget(strayWidget);
|
|
1595
|
+
(0, chai_1.expect)(strayWidget.isAttached).to.be.true;
|
|
1596
|
+
getWidgetsStub.withArgs('left').returns([strayWidget]);
|
|
1597
|
+
getWidgetsStub.withArgs('right').returns([]);
|
|
1598
|
+
const widgetIds = new Set();
|
|
1599
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1600
|
+
service.detachStrayWidgets(widgetIds);
|
|
1601
|
+
(0, chai_1.expect)(strayWidget.isAttached).to.be.false;
|
|
1602
|
+
panel.dispose();
|
|
1603
|
+
});
|
|
1604
|
+
it('should detach a right-panel widget whose ID is not in the layout ID set', () => {
|
|
1605
|
+
const strayWidget = new widgets_1.Widget();
|
|
1606
|
+
strayWidget.id = 'stray-right';
|
|
1607
|
+
const panel = new widgets_1.Panel();
|
|
1608
|
+
widgets_1.Widget.attach(panel, document.body);
|
|
1609
|
+
panel.addWidget(strayWidget);
|
|
1610
|
+
(0, chai_1.expect)(strayWidget.isAttached).to.be.true;
|
|
1611
|
+
getWidgetsStub.withArgs('left').returns([]);
|
|
1612
|
+
getWidgetsStub.withArgs('right').returns([strayWidget]);
|
|
1613
|
+
const widgetIds = new Set();
|
|
1614
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1615
|
+
service.detachStrayWidgets(widgetIds);
|
|
1616
|
+
(0, chai_1.expect)(strayWidget.isAttached).to.be.false;
|
|
1617
|
+
panel.dispose();
|
|
1618
|
+
});
|
|
1619
|
+
it('should NOT detach a widget whose ID IS in the layout ID set', () => {
|
|
1620
|
+
const keptWidget = new widgets_1.Widget();
|
|
1621
|
+
keptWidget.id = 'kept-widget';
|
|
1622
|
+
const panel = new widgets_1.Panel();
|
|
1623
|
+
widgets_1.Widget.attach(panel, document.body);
|
|
1624
|
+
panel.addWidget(keptWidget);
|
|
1625
|
+
(0, chai_1.expect)(keptWidget.isAttached).to.be.true;
|
|
1626
|
+
getWidgetsStub.withArgs('left').returns([keptWidget]);
|
|
1627
|
+
getWidgetsStub.withArgs('right').returns([]);
|
|
1628
|
+
const widgetIds = new Set(['kept-widget']);
|
|
1629
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1630
|
+
service.detachStrayWidgets(widgetIds);
|
|
1631
|
+
(0, chai_1.expect)(keptWidget.isAttached).to.be.true;
|
|
1632
|
+
panel.dispose();
|
|
1633
|
+
});
|
|
1634
|
+
it('should handle empty side panels gracefully', () => {
|
|
1635
|
+
getWidgetsStub.withArgs('left').returns([]);
|
|
1636
|
+
getWidgetsStub.withArgs('right').returns([]);
|
|
1637
|
+
const widgetIds = new Set();
|
|
1638
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1639
|
+
(0, chai_1.expect)(() => service.detachStrayWidgets(widgetIds)).to.not.throw();
|
|
1640
|
+
});
|
|
1641
|
+
it('should detach non-viewPlacements widget that leaked from another perspective', () => {
|
|
1642
|
+
// Simulate: Outline widget is in the left panel but not in the target layout
|
|
1643
|
+
const outlineWidget = new widgets_1.Widget();
|
|
1644
|
+
outlineWidget.id = 'outline-view';
|
|
1645
|
+
const panel = new widgets_1.Panel();
|
|
1646
|
+
widgets_1.Widget.attach(panel, document.body);
|
|
1647
|
+
panel.addWidget(outlineWidget);
|
|
1648
|
+
getWidgetsStub.withArgs('left').returns([outlineWidget]);
|
|
1649
|
+
getWidgetsStub.withArgs('right').returns([]);
|
|
1650
|
+
// Target layout has no widgets — outline should be detached
|
|
1651
|
+
const widgetIds = new Set();
|
|
1652
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1653
|
+
service.detachStrayWidgets(widgetIds);
|
|
1654
|
+
(0, chai_1.expect)(outlineWidget.isAttached).to.be.false;
|
|
1655
|
+
panel.dispose();
|
|
1656
|
+
});
|
|
1657
|
+
it('should detach multiple stray widgets from both panels', () => {
|
|
1658
|
+
const leftStray = new widgets_1.Widget();
|
|
1659
|
+
leftStray.id = 'left-stray';
|
|
1660
|
+
const rightStray = new widgets_1.Widget();
|
|
1661
|
+
rightStray.id = 'right-stray';
|
|
1662
|
+
const leftPanel = new widgets_1.Panel();
|
|
1663
|
+
const rightPanel = new widgets_1.Panel();
|
|
1664
|
+
widgets_1.Widget.attach(leftPanel, document.body);
|
|
1665
|
+
widgets_1.Widget.attach(rightPanel, document.body);
|
|
1666
|
+
leftPanel.addWidget(leftStray);
|
|
1667
|
+
rightPanel.addWidget(rightStray);
|
|
1668
|
+
getWidgetsStub.withArgs('left').returns([leftStray]);
|
|
1669
|
+
getWidgetsStub.withArgs('right').returns([rightStray]);
|
|
1670
|
+
const widgetIds = new Set();
|
|
1671
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1672
|
+
service.detachStrayWidgets(widgetIds);
|
|
1673
|
+
(0, chai_1.expect)(leftStray.isAttached).to.be.false;
|
|
1674
|
+
(0, chai_1.expect)(rightStray.isAttached).to.be.false;
|
|
1675
|
+
leftPanel.dispose();
|
|
1676
|
+
rightPanel.dispose();
|
|
1677
|
+
});
|
|
1678
|
+
describe('stray widget cleanup during perspective switch', () => {
|
|
1679
|
+
let parentPanel;
|
|
1680
|
+
beforeEach(() => {
|
|
1681
|
+
parentPanel = new widgets_1.Panel();
|
|
1682
|
+
widgets_1.Widget.attach(parentPanel, document.body);
|
|
1683
|
+
});
|
|
1684
|
+
afterEach(() => {
|
|
1685
|
+
parentPanel.dispose();
|
|
1686
|
+
});
|
|
1687
|
+
it('should detach stray widgets when restoring a saved layout', async () => {
|
|
1688
|
+
const chatWidget = new widgets_1.Widget();
|
|
1689
|
+
chatWidget.id = 'chat-view';
|
|
1690
|
+
parentPanel.addWidget(chatWidget);
|
|
1691
|
+
getOrCreateWidgetStub.withArgs('chat-view').resolves(chatWidget);
|
|
1692
|
+
service.registerPerspective({
|
|
1693
|
+
id: 'perspA',
|
|
1694
|
+
label: 'A',
|
|
1695
|
+
viewPlacements: new Map([['chat-view', 'main']])
|
|
1696
|
+
});
|
|
1697
|
+
service.registerPerspective({
|
|
1698
|
+
id: 'perspB',
|
|
1699
|
+
label: 'B',
|
|
1700
|
+
viewPlacements: new Map()
|
|
1701
|
+
});
|
|
1702
|
+
// Activate A (first time — viewPlacements applied)
|
|
1703
|
+
await service.switchPerspective('perspA');
|
|
1704
|
+
// Simulate saving A's layout WITHOUT chat-view (user closed it)
|
|
1705
|
+
const layoutAWithoutChat = {
|
|
1706
|
+
mainPanel: {
|
|
1707
|
+
main: {
|
|
1708
|
+
type: 'tab-area',
|
|
1709
|
+
widgets: [],
|
|
1710
|
+
currentIndex: -1
|
|
1711
|
+
}
|
|
1712
|
+
},
|
|
1713
|
+
bottomPanel: undefined
|
|
1714
|
+
};
|
|
1715
|
+
getLayoutDataStub.returns(layoutAWithoutChat);
|
|
1716
|
+
// Switch to B (saves A's layout without chat)
|
|
1717
|
+
await service.switchPerspective('perspB');
|
|
1718
|
+
// chat-view is still attached (simulating the stray scenario)
|
|
1719
|
+
(0, chai_1.expect)(chatWidget.isAttached).to.be.true;
|
|
1720
|
+
// When switching back to A, shell.getWidgets returns the stray
|
|
1721
|
+
getWidgetsStub.withArgs('left').returns([chatWidget]);
|
|
1722
|
+
getWidgetsStub.withArgs('right').returns([]);
|
|
1723
|
+
// Switch back to A — should restore layout and detach stray chat-view
|
|
1724
|
+
await service.switchPerspective('perspA');
|
|
1725
|
+
(0, chai_1.expect)(chatWidget.isAttached).to.be.false;
|
|
1726
|
+
});
|
|
1727
|
+
it('should NOT detach a widget that IS in the saved layout (user customization preserved)', async () => {
|
|
1728
|
+
const chatWidget = new widgets_1.Widget();
|
|
1729
|
+
chatWidget.id = 'chat-view';
|
|
1730
|
+
parentPanel.addWidget(chatWidget);
|
|
1731
|
+
getOrCreateWidgetStub.withArgs('chat-view').resolves(chatWidget);
|
|
1732
|
+
service.registerPerspective({
|
|
1733
|
+
id: 'perspA',
|
|
1734
|
+
label: 'A',
|
|
1735
|
+
viewPlacements: new Map([['chat-view', 'main']])
|
|
1736
|
+
});
|
|
1737
|
+
service.registerPerspective({
|
|
1738
|
+
id: 'perspB',
|
|
1739
|
+
label: 'B',
|
|
1740
|
+
viewPlacements: new Map()
|
|
1741
|
+
});
|
|
1742
|
+
// Activate A
|
|
1743
|
+
await service.switchPerspective('perspA');
|
|
1744
|
+
// Simulate saving A's layout WITH chat-view in right panel
|
|
1745
|
+
const layoutAWithChat = {
|
|
1746
|
+
rightPanel: {
|
|
1747
|
+
type: 'sidepanel',
|
|
1748
|
+
items: [{ widget: chatWidget, rank: 0, expanded: true }]
|
|
1749
|
+
},
|
|
1750
|
+
mainPanel: undefined,
|
|
1751
|
+
bottomPanel: undefined
|
|
1752
|
+
};
|
|
1753
|
+
getLayoutDataStub.returns(layoutAWithChat);
|
|
1754
|
+
// Switch to B (saves A's layout with chat)
|
|
1755
|
+
await service.switchPerspective('perspB');
|
|
1756
|
+
// When switching back, chat is in the right panel
|
|
1757
|
+
getWidgetsStub.withArgs('left').returns([]);
|
|
1758
|
+
getWidgetsStub.withArgs('right').returns([chatWidget]);
|
|
1759
|
+
// Switch back to A — chat IS in layout, should NOT be detached
|
|
1760
|
+
await service.switchPerspective('perspA');
|
|
1761
|
+
(0, chai_1.expect)(chatWidget.isAttached).to.be.true;
|
|
1762
|
+
});
|
|
1763
|
+
it('should detach non-viewPlacements widget leaking from another perspective', async () => {
|
|
1764
|
+
// Outline is not in AI First's viewPlacements
|
|
1765
|
+
const outlineWidget = new widgets_1.Widget();
|
|
1766
|
+
outlineWidget.id = 'outline-view';
|
|
1767
|
+
parentPanel.addWidget(outlineWidget);
|
|
1768
|
+
service.registerPerspective({
|
|
1769
|
+
id: 'ai-first',
|
|
1770
|
+
label: 'AI First',
|
|
1771
|
+
viewPlacements: new Map([['chat-view', 'main']])
|
|
1772
|
+
});
|
|
1773
|
+
service.registerPerspective({
|
|
1774
|
+
id: 'default-persp',
|
|
1775
|
+
label: 'Default',
|
|
1776
|
+
viewPlacements: new Map()
|
|
1777
|
+
});
|
|
1778
|
+
// Activate AI First
|
|
1779
|
+
await service.switchPerspective('ai-first');
|
|
1780
|
+
// Save AI First layout without outline
|
|
1781
|
+
const aiLayout = {
|
|
1782
|
+
mainPanel: {
|
|
1783
|
+
main: {
|
|
1784
|
+
type: 'tab-area',
|
|
1785
|
+
widgets: [],
|
|
1786
|
+
currentIndex: -1
|
|
1787
|
+
}
|
|
1788
|
+
},
|
|
1789
|
+
bottomPanel: undefined
|
|
1790
|
+
};
|
|
1791
|
+
getLayoutDataStub.returns(aiLayout);
|
|
1792
|
+
// Switch to default (saves AI First layout)
|
|
1793
|
+
await service.switchPerspective('default-persp');
|
|
1794
|
+
// Outline is open in default's left panel
|
|
1795
|
+
(0, chai_1.expect)(outlineWidget.isAttached).to.be.true;
|
|
1796
|
+
// Switch back to AI First — outline is a stray
|
|
1797
|
+
getWidgetsStub.withArgs('left').returns([outlineWidget]);
|
|
1798
|
+
getWidgetsStub.withArgs('right').returns([]);
|
|
1799
|
+
await service.switchPerspective('ai-first');
|
|
1800
|
+
// Outline should be detached (not in AI First's saved layout)
|
|
1801
|
+
(0, chai_1.expect)(outlineWidget.isAttached).to.be.false;
|
|
1802
|
+
});
|
|
1803
|
+
});
|
|
1804
|
+
});
|
|
1805
|
+
});
|
|
1806
|
+
//# sourceMappingURL=perspective-service.spec.js.map
|