@theia/core 1.74.0-next.8 → 1.74.0
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 +419 -41
- 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.map +1 -1
- package/lib/browser/common-frontend-contribution.js +7 -7
- package/lib/browser/common-frontend-contribution.js.map +1 -1
- package/lib/browser/frontend-application-module.d.ts.map +1 -1
- package/lib/browser/frontend-application-module.js +9 -0
- 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-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/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 +2 -2
- package/lib/browser/widgets/select-component.d.ts.map +1 -1
- package/lib/browser/widgets/select-component.js +42 -12
- 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-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/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-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.ts +7 -7
- package/src/browser/frontend-application-module.ts +11 -1
- 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-widget.tsx +12 -78
- package/src/browser/widgets/index.ts +1 -0
- package/src/browser/widgets/select-component.tsx +41 -12
- package/src/browser/widgets/tab-bar-tracker.spec.ts +121 -0
- package/src/browser/widgets/tab-bar-tracker.ts +68 -0
- 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-style.css +3 -1
- package/src/electron-browser/preload.ts +5 -2
- 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,746 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '../test/jsdom';
|
|
18
|
+
const disableJSDOM = enableJSDOM();
|
|
19
|
+
|
|
20
|
+
import { expect } from 'chai';
|
|
21
|
+
import * as sinon from 'sinon';
|
|
22
|
+
import {
|
|
23
|
+
ShellLayoutRestorer,
|
|
24
|
+
PersistedPerspectiveData,
|
|
25
|
+
PERSPECTIVE_LAYOUTS_STORAGE_KEY
|
|
26
|
+
} from './shell-layout-restorer';
|
|
27
|
+
import { ApplicationShell } from './application-shell';
|
|
28
|
+
disableJSDOM();
|
|
29
|
+
|
|
30
|
+
describe('ShellLayoutRestorer - Perspective Support', () => {
|
|
31
|
+
let restorer: ShellLayoutRestorer;
|
|
32
|
+
let mockStorageService: { setData: sinon.SinonStub; getData: sinon.SinonStub };
|
|
33
|
+
let mockLogger: { info: sinon.SinonStub; error: sinon.SinonStub; warn: sinon.SinonStub };
|
|
34
|
+
let mockWidgetManager: { getDescription: sinon.SinonStub };
|
|
35
|
+
let mockShell: {
|
|
36
|
+
getLayoutData: sinon.SinonStub;
|
|
37
|
+
setLayoutData: sinon.SinonStub;
|
|
38
|
+
};
|
|
39
|
+
let mockApp: { shell: typeof mockShell };
|
|
40
|
+
let mockProvider: {
|
|
41
|
+
getActivePerspectiveId: sinon.SinonStub;
|
|
42
|
+
getSavedPerspectiveIds: sinon.SinonStub;
|
|
43
|
+
getSavedLayout: sinon.SinonStub;
|
|
44
|
+
setSavedLayout: sinon.SinonStub;
|
|
45
|
+
setActivePerspectiveId: sinon.SinonStub;
|
|
46
|
+
clearSavedLayouts: sinon.SinonStub;
|
|
47
|
+
onLayoutRestored: sinon.SinonStub;
|
|
48
|
+
defaultPerspectiveId: string;
|
|
49
|
+
};
|
|
50
|
+
let mockTransformations: { getContributions: sinon.SinonStub };
|
|
51
|
+
let mockMigrations: { getContributions: sinon.SinonStub };
|
|
52
|
+
let mockWindowService: { isSafeToShutDown: sinon.SinonStub; reload: sinon.SinonStub };
|
|
53
|
+
let mockThemeService: { reset: sinon.SinonStub };
|
|
54
|
+
let toTearDown: () => void;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
toTearDown = enableJSDOM();
|
|
58
|
+
|
|
59
|
+
mockStorageService = {
|
|
60
|
+
setData: sinon.stub().resolves(),
|
|
61
|
+
getData: sinon.stub().resolves(undefined)
|
|
62
|
+
};
|
|
63
|
+
mockLogger = {
|
|
64
|
+
info: sinon.stub(),
|
|
65
|
+
error: sinon.stub(),
|
|
66
|
+
warn: sinon.stub()
|
|
67
|
+
};
|
|
68
|
+
mockWidgetManager = {
|
|
69
|
+
getDescription: sinon.stub().returns(undefined)
|
|
70
|
+
};
|
|
71
|
+
mockShell = {
|
|
72
|
+
getLayoutData: sinon.stub().returns({ version: 999, mainPanel: {}, bottomPanel: {} }),
|
|
73
|
+
setLayoutData: sinon.stub().resolves()
|
|
74
|
+
};
|
|
75
|
+
mockApp = { shell: mockShell };
|
|
76
|
+
mockProvider = {
|
|
77
|
+
getActivePerspectiveId: sinon.stub().returns('default'),
|
|
78
|
+
getSavedPerspectiveIds: sinon.stub().returns([]),
|
|
79
|
+
getSavedLayout: sinon.stub().returns(undefined),
|
|
80
|
+
setSavedLayout: sinon.stub(),
|
|
81
|
+
setActivePerspectiveId: sinon.stub().returns(true),
|
|
82
|
+
clearSavedLayouts: sinon.stub(),
|
|
83
|
+
onLayoutRestored: sinon.stub(),
|
|
84
|
+
defaultPerspectiveId: 'default'
|
|
85
|
+
};
|
|
86
|
+
mockTransformations = {
|
|
87
|
+
getContributions: sinon.stub().returns([])
|
|
88
|
+
};
|
|
89
|
+
mockMigrations = {
|
|
90
|
+
getContributions: sinon.stub().returns([])
|
|
91
|
+
};
|
|
92
|
+
mockWindowService = {
|
|
93
|
+
isSafeToShutDown: sinon.stub().resolves(true),
|
|
94
|
+
reload: sinon.stub()
|
|
95
|
+
};
|
|
96
|
+
mockThemeService = {
|
|
97
|
+
reset: sinon.stub()
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
restorer = new ShellLayoutRestorer(
|
|
101
|
+
mockWidgetManager as never,
|
|
102
|
+
mockLogger as never,
|
|
103
|
+
mockStorageService as never
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
(restorer as unknown as Record<string, unknown>)['perspectiveService'] = mockProvider;
|
|
107
|
+
(restorer as unknown as Record<string, unknown>)['transformations'] = mockTransformations;
|
|
108
|
+
(restorer as unknown as Record<string, unknown>)['migrations'] = mockMigrations;
|
|
109
|
+
(restorer as unknown as Record<string, unknown>)['windowService'] = mockWindowService;
|
|
110
|
+
(restorer as unknown as Record<string, unknown>)['themeService'] = mockThemeService;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
sinon.restore();
|
|
115
|
+
toTearDown();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('storePerspectiveLayouts', () => {
|
|
119
|
+
it('should read current shell layout as the active perspective layout', () => {
|
|
120
|
+
|
|
121
|
+
mockProvider.getActivePerspectiveId.returns('persp-a');
|
|
122
|
+
mockProvider.getSavedPerspectiveIds.returns([]);
|
|
123
|
+
const currentLayout = { version: 999, mainPanel: { items: ['editor1'] }, bottomPanel: {} };
|
|
124
|
+
mockShell.getLayoutData.returns(currentLayout);
|
|
125
|
+
|
|
126
|
+
restorer.storeLayout(mockApp as never);
|
|
127
|
+
|
|
128
|
+
const perspCall = mockStorageService.setData.getCalls().find(
|
|
129
|
+
(c: sinon.SinonSpyCall) => c.args[0] === PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] !== undefined
|
|
130
|
+
);
|
|
131
|
+
expect(perspCall).to.not.be.undefined;
|
|
132
|
+
const data = perspCall!.args[1] as PersistedPerspectiveData;
|
|
133
|
+
expect(data.activePerspectiveId).to.equal('persp-a');
|
|
134
|
+
expect(data.layouts['persp-a']).to.be.a('string');
|
|
135
|
+
// Verify it contains the current shell layout content
|
|
136
|
+
const parsed = JSON.parse(data.layouts['persp-a']);
|
|
137
|
+
expect(parsed.mainPanel.items).to.deep.equal(['editor1']);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should deflate all inactive perspective layouts from provider', () => {
|
|
141
|
+
|
|
142
|
+
mockProvider.getActivePerspectiveId.returns('active');
|
|
143
|
+
mockProvider.getSavedPerspectiveIds.returns(['active', 'inactive1', 'inactive2']);
|
|
144
|
+
const inactiveLayout1 = { version: 999, mainPanel: { items: ['w1'] }, bottomPanel: {} } as unknown as ApplicationShell.LayoutData;
|
|
145
|
+
const inactiveLayout2 = { version: 999, mainPanel: { items: ['w2'] }, bottomPanel: {} } as unknown as ApplicationShell.LayoutData;
|
|
146
|
+
mockProvider.getSavedLayout.withArgs('inactive1').returns(inactiveLayout1);
|
|
147
|
+
mockProvider.getSavedLayout.withArgs('inactive2').returns(inactiveLayout2);
|
|
148
|
+
mockProvider.getSavedLayout.withArgs('active').returns(undefined);
|
|
149
|
+
|
|
150
|
+
restorer.storeLayout(mockApp as never);
|
|
151
|
+
|
|
152
|
+
const [, data] = mockStorageService.setData.firstCall.args as [string, PersistedPerspectiveData];
|
|
153
|
+
expect(Object.keys(data.layouts)).to.have.lengthOf(3);
|
|
154
|
+
expect(data.layouts).to.have.property('active');
|
|
155
|
+
expect(data.layouts).to.have.property('inactive1');
|
|
156
|
+
expect(data.layouts).to.have.property('inactive2');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should use shell layout for active perspective, not provider', () => {
|
|
160
|
+
|
|
161
|
+
mockProvider.getActivePerspectiveId.returns('active');
|
|
162
|
+
mockProvider.getSavedPerspectiveIds.returns(['active']);
|
|
163
|
+
// Provider has stale data for the active perspective
|
|
164
|
+
const staleLayout = { version: 999, mainPanel: { items: ['stale'] }, bottomPanel: {} } as unknown as ApplicationShell.LayoutData;
|
|
165
|
+
mockProvider.getSavedLayout.withArgs('active').returns(staleLayout);
|
|
166
|
+
// Shell has the current data
|
|
167
|
+
const currentLayout = { version: 999, mainPanel: { items: ['current'] }, bottomPanel: {} };
|
|
168
|
+
mockShell.getLayoutData.returns(currentLayout);
|
|
169
|
+
|
|
170
|
+
restorer.storeLayout(mockApp as never);
|
|
171
|
+
|
|
172
|
+
const [, data] = mockStorageService.setData.firstCall.args as [string, PersistedPerspectiveData];
|
|
173
|
+
const parsed = JSON.parse(data.layouts['active']);
|
|
174
|
+
// Should use shell data, not provider data
|
|
175
|
+
expect(parsed.mainPanel.items).to.deep.equal(['current']);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should write to legacy key when only default perspective is used', () => {
|
|
179
|
+
mockProvider.getActivePerspectiveId.returns('default');
|
|
180
|
+
mockProvider.getSavedPerspectiveIds.returns([]);
|
|
181
|
+
|
|
182
|
+
restorer.storeLayout(mockApp as never);
|
|
183
|
+
|
|
184
|
+
// Should write to legacy key for backward compatibility
|
|
185
|
+
const legacyCall = mockStorageService.setData.getCalls().find(
|
|
186
|
+
(c: sinon.SinonSpyCall) => c.args[0] === 'layout'
|
|
187
|
+
);
|
|
188
|
+
expect(legacyCall).to.not.be.undefined;
|
|
189
|
+
// Should clear the perspective key
|
|
190
|
+
const perspClear = mockStorageService.setData.getCalls().find(
|
|
191
|
+
(c: sinon.SinonSpyCall) => c.args[0] === PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] === undefined
|
|
192
|
+
);
|
|
193
|
+
expect(perspClear).to.not.be.undefined;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should write to perspective key when multiple perspectives are used', () => {
|
|
197
|
+
mockProvider.getActivePerspectiveId.returns('persp-a');
|
|
198
|
+
mockProvider.getSavedPerspectiveIds.returns(['persp-a', 'persp-b']);
|
|
199
|
+
const inactiveLayout = { version: 999, mainPanel: { items: ['b'] }, bottomPanel: {} } as unknown as ApplicationShell.LayoutData;
|
|
200
|
+
mockProvider.getSavedLayout.withArgs('persp-b').returns(inactiveLayout);
|
|
201
|
+
mockProvider.getSavedLayout.withArgs('persp-a').returns(undefined);
|
|
202
|
+
|
|
203
|
+
restorer.storeLayout(mockApp as never);
|
|
204
|
+
|
|
205
|
+
const perspCall = mockStorageService.setData.getCalls().find(
|
|
206
|
+
(c: sinon.SinonSpyCall) => c.args[0] === PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] !== undefined
|
|
207
|
+
);
|
|
208
|
+
expect(perspCall).to.not.be.undefined;
|
|
209
|
+
// Should clear the legacy key
|
|
210
|
+
const legacyClear = mockStorageService.setData.getCalls().find(
|
|
211
|
+
(c: sinon.SinonSpyCall) => c.args[0] === 'layout' && c.args[1] === undefined
|
|
212
|
+
);
|
|
213
|
+
expect(legacyClear).to.not.be.undefined;
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('restorePerspectiveLayouts', () => {
|
|
218
|
+
it('should inflate all layouts and push to provider', async () => {
|
|
219
|
+
|
|
220
|
+
const persisted: PersistedPerspectiveData = {
|
|
221
|
+
activePerspectiveId: 'persp-a',
|
|
222
|
+
layouts: {
|
|
223
|
+
'persp-a': JSON.stringify({ version: 999, mainPanel: { items: ['a'] }, bottomPanel: {} }),
|
|
224
|
+
'persp-b': JSON.stringify({ version: 999, mainPanel: { items: ['b'] }, bottomPanel: {} })
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
228
|
+
|
|
229
|
+
// Return the inflated layout for the active persp when asked
|
|
230
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
231
|
+
if (id === 'persp-a') {
|
|
232
|
+
// After setSavedLayout is called, return the layout
|
|
233
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
234
|
+
(c: sinon.SinonSpyCall) => c.args[0] === 'persp-a'
|
|
235
|
+
);
|
|
236
|
+
return call?.args[1];
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const result = await restorer.restoreLayout(mockApp as never);
|
|
242
|
+
|
|
243
|
+
expect(result).to.be.true;
|
|
244
|
+
expect(mockProvider.setSavedLayout.callCount).to.equal(2);
|
|
245
|
+
// Verify persp-a was set
|
|
246
|
+
const setACall = mockProvider.setSavedLayout.getCalls().find(
|
|
247
|
+
(c: sinon.SinonSpyCall) => c.args[0] === 'persp-a'
|
|
248
|
+
);
|
|
249
|
+
expect(setACall).to.not.be.undefined;
|
|
250
|
+
// Verify persp-b was set
|
|
251
|
+
const setBCall = mockProvider.setSavedLayout.getCalls().find(
|
|
252
|
+
(c: sinon.SinonSpyCall) => c.args[0] === 'persp-b'
|
|
253
|
+
);
|
|
254
|
+
expect(setBCall).to.not.be.undefined;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should set active perspective ID on provider', async () => {
|
|
258
|
+
|
|
259
|
+
const persisted: PersistedPerspectiveData = {
|
|
260
|
+
activePerspectiveId: 'my-persp',
|
|
261
|
+
layouts: {
|
|
262
|
+
'my-persp': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} })
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
266
|
+
|
|
267
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
268
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
269
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
270
|
+
);
|
|
271
|
+
return call?.args[1];
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await restorer.restoreLayout(mockApp as never);
|
|
275
|
+
|
|
276
|
+
expect(mockProvider.setActivePerspectiveId.calledWith('my-persp')).to.be.true;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should apply the active layout to the shell', async () => {
|
|
280
|
+
|
|
281
|
+
const layoutData = { version: 999, mainPanel: { items: ['active-item'] }, bottomPanel: {} };
|
|
282
|
+
const persisted: PersistedPerspectiveData = {
|
|
283
|
+
activePerspectiveId: 'active',
|
|
284
|
+
layouts: {
|
|
285
|
+
'active': JSON.stringify(layoutData)
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
289
|
+
|
|
290
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
291
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
292
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
293
|
+
);
|
|
294
|
+
return call?.args[1];
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await restorer.restoreLayout(mockApp as never);
|
|
298
|
+
|
|
299
|
+
expect(mockShell.setLayoutData.calledOnce).to.be.true;
|
|
300
|
+
const applied = mockShell.setLayoutData.firstCall.args[0];
|
|
301
|
+
expect(applied.mainPanel.items).to.deep.equal(['active-item']);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should apply transformations to all inflated layouts, not just the active one', async () => {
|
|
305
|
+
|
|
306
|
+
const transformer = {
|
|
307
|
+
transformLayoutOnRestore: sinon.stub()
|
|
308
|
+
};
|
|
309
|
+
mockTransformations.getContributions.returns([transformer]);
|
|
310
|
+
|
|
311
|
+
const persisted: PersistedPerspectiveData = {
|
|
312
|
+
activePerspectiveId: 'persp-a',
|
|
313
|
+
layouts: {
|
|
314
|
+
'persp-a': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }),
|
|
315
|
+
'persp-b': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} })
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
319
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
320
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
321
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
322
|
+
);
|
|
323
|
+
return call?.args[1];
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await restorer.restoreLayout(mockApp as never);
|
|
327
|
+
|
|
328
|
+
// Transformer should be called for both layouts
|
|
329
|
+
expect(transformer.transformLayoutOnRestore.callCount).to.equal(2);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should fall back to provider active ID when persisted ID is not registered', async () => {
|
|
333
|
+
|
|
334
|
+
const persisted: PersistedPerspectiveData = {
|
|
335
|
+
activePerspectiveId: 'removed-persp',
|
|
336
|
+
layouts: {
|
|
337
|
+
'removed-persp': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }),
|
|
338
|
+
'default': JSON.stringify({ version: 999, mainPanel: { items: ['default-w'] }, bottomPanel: {} })
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
342
|
+
mockProvider.setActivePerspectiveId.returns(false);
|
|
343
|
+
mockProvider.getActivePerspectiveId.returns('default');
|
|
344
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
345
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
346
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
347
|
+
);
|
|
348
|
+
return call?.args[1];
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const result = await restorer.restoreLayout(mockApp as never);
|
|
352
|
+
|
|
353
|
+
expect(result).to.be.true;
|
|
354
|
+
// Should apply 'default' layout to shell, not 'removed-persp'
|
|
355
|
+
const applied = mockShell.setLayoutData.firstCall.args[0];
|
|
356
|
+
expect(applied.mainPanel.items).to.deep.equal(['default-w']);
|
|
357
|
+
// onLayoutRestored should be called with 'default'
|
|
358
|
+
expect(mockProvider.onLayoutRestored.calledWith('default')).to.be.true;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should return false when no perspective data and no legacy data exists', async () => {
|
|
362
|
+
|
|
363
|
+
mockStorageService.getData.resolves(undefined);
|
|
364
|
+
|
|
365
|
+
const result = await restorer.restoreLayout(mockApp as never);
|
|
366
|
+
|
|
367
|
+
expect(result).to.be.false;
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should warn when a perspective layout fails to inflate', async () => {
|
|
371
|
+
|
|
372
|
+
const persisted: PersistedPerspectiveData = {
|
|
373
|
+
activePerspectiveId: 'good',
|
|
374
|
+
layouts: {
|
|
375
|
+
'good': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }),
|
|
376
|
+
'bad': 'invalid-json{'
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
380
|
+
|
|
381
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
382
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
383
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
384
|
+
);
|
|
385
|
+
return call?.args[1];
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
await restorer.restoreLayout(mockApp as never);
|
|
389
|
+
|
|
390
|
+
// 'good' should be inflated successfully
|
|
391
|
+
const goodCall = mockProvider.setSavedLayout.getCalls().find(
|
|
392
|
+
(c: sinon.SinonSpyCall) => c.args[0] === 'good'
|
|
393
|
+
);
|
|
394
|
+
expect(goodCall).to.not.be.undefined;
|
|
395
|
+
|
|
396
|
+
// Should warn about 'bad'
|
|
397
|
+
expect(mockLogger.warn.called).to.be.true;
|
|
398
|
+
const warnCall = mockLogger.warn.getCalls().find(
|
|
399
|
+
(c: sinon.SinonSpyCall) => (c.args[0] as string).includes('bad')
|
|
400
|
+
);
|
|
401
|
+
expect(warnCall).to.not.be.undefined;
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('legacy migration', () => {
|
|
406
|
+
it('should fall back to legacy key when perspective-layouts is absent', async () => {
|
|
407
|
+
|
|
408
|
+
const legacyLayout = JSON.stringify({ version: 999, mainPanel: { items: ['legacy'] }, bottomPanel: {} });
|
|
409
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(undefined);
|
|
410
|
+
mockStorageService.getData.withArgs('layout').resolves(legacyLayout);
|
|
411
|
+
|
|
412
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
413
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
414
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
415
|
+
);
|
|
416
|
+
return call?.args[1];
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const result = await restorer.restoreLayout(mockApp as never);
|
|
420
|
+
|
|
421
|
+
expect(result).to.be.true;
|
|
422
|
+
// Should push layout as 'default' perspective
|
|
423
|
+
expect(mockProvider.setSavedLayout.calledOnce).to.be.true;
|
|
424
|
+
expect(mockProvider.setSavedLayout.firstCall.args[0]).to.equal('default');
|
|
425
|
+
// Should set active to 'default'
|
|
426
|
+
expect(mockProvider.setActivePerspectiveId.calledWith('default')).to.be.true;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should keep the legacy key after a successful migration', async () => {
|
|
430
|
+
|
|
431
|
+
const legacyLayout = JSON.stringify({ version: 999, mainPanel: { items: ['legacy'] }, bottomPanel: {} });
|
|
432
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(undefined);
|
|
433
|
+
mockStorageService.getData.withArgs('layout').resolves(legacyLayout);
|
|
434
|
+
|
|
435
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
436
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
437
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
438
|
+
);
|
|
439
|
+
return call?.args[1];
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
await restorer.restoreLayout(mockApp as never);
|
|
443
|
+
|
|
444
|
+
// Restoring must not touch the key: it is rewritten or cleared by the next `storeLayout`,
|
|
445
|
+
// so a crash before then still finds the layout where it was.
|
|
446
|
+
const legacyWrite = mockStorageService.setData.getCalls().find(
|
|
447
|
+
(c: sinon.SinonSpyCall) => c.args[0] === 'layout'
|
|
448
|
+
);
|
|
449
|
+
expect(legacyWrite).to.be.undefined;
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should preserve legacy key when inflate fails', async () => {
|
|
453
|
+
|
|
454
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(undefined);
|
|
455
|
+
mockStorageService.getData.withArgs('layout').resolves('not-valid-json{');
|
|
456
|
+
|
|
457
|
+
const result = await restorer.restoreLayout(mockApp as never);
|
|
458
|
+
|
|
459
|
+
expect(result).to.be.false;
|
|
460
|
+
// Legacy key should NOT be cleared
|
|
461
|
+
const legacyClear = mockStorageService.setData.getCalls().find(
|
|
462
|
+
(c: sinon.SinonSpyCall) => c.args[0] === 'layout' && c.args[1] === undefined
|
|
463
|
+
);
|
|
464
|
+
expect(legacyClear).to.be.undefined;
|
|
465
|
+
// No layout should have been saved
|
|
466
|
+
expect(mockProvider.setSavedLayout.called).to.be.false;
|
|
467
|
+
// Should warn about the failure
|
|
468
|
+
expect(mockLogger.warn.called).to.be.true;
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should apply migrated legacy layout to shell', async () => {
|
|
472
|
+
|
|
473
|
+
const legacyLayout = JSON.stringify({ version: 999, mainPanel: { items: ['legacy-w'] }, bottomPanel: {} });
|
|
474
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(undefined);
|
|
475
|
+
mockStorageService.getData.withArgs('layout').resolves(legacyLayout);
|
|
476
|
+
|
|
477
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
478
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
479
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
480
|
+
);
|
|
481
|
+
return call?.args[1];
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
await restorer.restoreLayout(mockApp as never);
|
|
485
|
+
|
|
486
|
+
expect(mockShell.setLayoutData.calledOnce).to.be.true;
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('storePerspectiveLayouts - error isolation', () => {
|
|
491
|
+
it('should persist other perspectives when one inactive perspective deflate fails', () => {
|
|
492
|
+
mockProvider.getActivePerspectiveId.returns('active');
|
|
493
|
+
mockProvider.getSavedPerspectiveIds.returns(['active', 'good', 'bad']);
|
|
494
|
+
|
|
495
|
+
const goodLayout = { version: 999, mainPanel: { items: ['good'] }, bottomPanel: {} } as unknown as ApplicationShell.LayoutData;
|
|
496
|
+
// Create a layout with a circular reference that will cause JSON.stringify to throw
|
|
497
|
+
const badLayout: Record<string, unknown> = { version: 999, mainPanel: {}, bottomPanel: {} };
|
|
498
|
+
badLayout['self'] = badLayout;
|
|
499
|
+
|
|
500
|
+
mockProvider.getSavedLayout.withArgs('good').returns(goodLayout);
|
|
501
|
+
mockProvider.getSavedLayout.withArgs('bad').returns(badLayout);
|
|
502
|
+
mockProvider.getSavedLayout.withArgs('active').returns(undefined);
|
|
503
|
+
|
|
504
|
+
restorer.storeLayout(mockApp as never);
|
|
505
|
+
|
|
506
|
+
// Multiple perspectives => writes to PERSPECTIVE_LAYOUTS_STORAGE_KEY and clears legacy
|
|
507
|
+
const perspCall = mockStorageService.setData.getCalls().find(
|
|
508
|
+
(c: sinon.SinonSpyCall) => c.args[0] === PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] !== undefined
|
|
509
|
+
);
|
|
510
|
+
expect(perspCall).to.not.be.undefined;
|
|
511
|
+
const data = perspCall!.args[1] as PersistedPerspectiveData;
|
|
512
|
+
expect(data.layouts).to.have.property('active');
|
|
513
|
+
expect(data.layouts).to.have.property('good');
|
|
514
|
+
expect(data.layouts).to.not.have.property('bad');
|
|
515
|
+
|
|
516
|
+
// Should warn about 'bad'
|
|
517
|
+
expect(mockLogger.warn.called).to.be.true;
|
|
518
|
+
const warnCall = mockLogger.warn.getCalls().find(
|
|
519
|
+
(c: sinon.SinonSpyCall) => (c.args[0] as string).includes('bad')
|
|
520
|
+
);
|
|
521
|
+
expect(warnCall).to.not.be.undefined;
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should persist inactive perspectives when active perspective deflate fails', () => {
|
|
525
|
+
mockProvider.getActivePerspectiveId.returns('active');
|
|
526
|
+
mockProvider.getSavedPerspectiveIds.returns(['active', 'inactive']);
|
|
527
|
+
|
|
528
|
+
// Make the shell return a circular structure for the active perspective
|
|
529
|
+
const circularLayout: Record<string, unknown> = { version: 999, mainPanel: {}, bottomPanel: {} };
|
|
530
|
+
circularLayout['self'] = circularLayout;
|
|
531
|
+
mockShell.getLayoutData.returns(circularLayout);
|
|
532
|
+
|
|
533
|
+
const inactiveLayout = { version: 999, mainPanel: { items: ['inactive'] }, bottomPanel: {} } as unknown as ApplicationShell.LayoutData;
|
|
534
|
+
mockProvider.getSavedLayout.withArgs('inactive').returns(inactiveLayout);
|
|
535
|
+
|
|
536
|
+
restorer.storeLayout(mockApp as never);
|
|
537
|
+
|
|
538
|
+
// Multiple perspectives (active + inactive) => writes to PERSPECTIVE_LAYOUTS_STORAGE_KEY
|
|
539
|
+
const perspCall = mockStorageService.setData.getCalls().find(
|
|
540
|
+
(c: sinon.SinonSpyCall) => c.args[0] === PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] !== undefined
|
|
541
|
+
);
|
|
542
|
+
expect(perspCall).to.not.be.undefined;
|
|
543
|
+
const data = perspCall!.args[1] as PersistedPerspectiveData;
|
|
544
|
+
expect(data.layouts).to.not.have.property('active');
|
|
545
|
+
expect(data.layouts).to.have.property('inactive');
|
|
546
|
+
|
|
547
|
+
expect(mockLogger.warn.called).to.be.true;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should clear storage key when setData rejects (multi-perspective)', async () => {
|
|
551
|
+
mockProvider.getActivePerspectiveId.returns('persp-a');
|
|
552
|
+
mockProvider.getSavedPerspectiveIds.returns(['persp-a', 'persp-b']);
|
|
553
|
+
const layout = { version: 999, mainPanel: {}, bottomPanel: {} } as unknown as ApplicationShell.LayoutData;
|
|
554
|
+
mockProvider.getSavedLayout.withArgs('persp-b').returns(layout);
|
|
555
|
+
|
|
556
|
+
// The perspective-layouts setData call rejects only the first time (when data is present)
|
|
557
|
+
let perspCallCount = 0;
|
|
558
|
+
mockStorageService.setData.callsFake((key: string, data: unknown) => {
|
|
559
|
+
if (key === PERSPECTIVE_LAYOUTS_STORAGE_KEY && data !== undefined) {
|
|
560
|
+
perspCallCount++;
|
|
561
|
+
return Promise.reject(new Error('storage error'));
|
|
562
|
+
}
|
|
563
|
+
return Promise.resolve();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
restorer.storeLayout(mockApp as never);
|
|
567
|
+
|
|
568
|
+
// Allow the .catch() handler to execute
|
|
569
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
570
|
+
|
|
571
|
+
expect(perspCallCount).to.equal(1);
|
|
572
|
+
// Should have attempted to clear the perspective key
|
|
573
|
+
const clearCall = mockStorageService.setData.getCalls().find(
|
|
574
|
+
(c: sinon.SinonSpyCall) => c.args[0] === PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] === undefined
|
|
575
|
+
);
|
|
576
|
+
expect(clearCall).to.not.be.undefined;
|
|
577
|
+
|
|
578
|
+
expect(mockLogger.error.called).to.be.true;
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe('restorePerspectiveLayouts - desync fallback', () => {
|
|
583
|
+
it('should fall back to default perspective when active perspective has no saved layout', async () => {
|
|
584
|
+
const persisted: PersistedPerspectiveData = {
|
|
585
|
+
activePerspectiveId: 'custom',
|
|
586
|
+
layouts: {
|
|
587
|
+
'default': JSON.stringify({ version: 999, mainPanel: { items: ['default-w'] }, bottomPanel: {} })
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
591
|
+
|
|
592
|
+
// 'custom' is a registered perspective, but has no layout entry
|
|
593
|
+
mockProvider.setActivePerspectiveId.returns(true);
|
|
594
|
+
|
|
595
|
+
// Track calls to setActivePerspectiveId to simulate state changes
|
|
596
|
+
let currentActiveId = 'default';
|
|
597
|
+
mockProvider.setActivePerspectiveId.callsFake((id: string) => {
|
|
598
|
+
currentActiveId = id;
|
|
599
|
+
return true;
|
|
600
|
+
});
|
|
601
|
+
mockProvider.getActivePerspectiveId.callsFake(() => currentActiveId);
|
|
602
|
+
|
|
603
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
604
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
605
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
606
|
+
);
|
|
607
|
+
return call?.args[1];
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const result = await restorer.restoreLayout(mockApp as never);
|
|
611
|
+
|
|
612
|
+
expect(result).to.be.true;
|
|
613
|
+
// Should have fallen back to default
|
|
614
|
+
expect(mockLogger.warn.called).to.be.true;
|
|
615
|
+
const warnCall = mockLogger.warn.getCalls().find(
|
|
616
|
+
(c: sinon.SinonSpyCall) => (c.args[0] as string).includes('custom')
|
|
617
|
+
);
|
|
618
|
+
expect(warnCall).to.not.be.undefined;
|
|
619
|
+
// Should have called onLayoutRestored with default
|
|
620
|
+
expect(mockProvider.onLayoutRestored.calledWith('default')).to.be.true;
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('should call onLayoutRestored even when falling through to no layout', async () => {
|
|
624
|
+
const persisted: PersistedPerspectiveData = {
|
|
625
|
+
activePerspectiveId: 'custom',
|
|
626
|
+
layouts: {}
|
|
627
|
+
};
|
|
628
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
629
|
+
|
|
630
|
+
let currentActiveId = 'default';
|
|
631
|
+
mockProvider.setActivePerspectiveId.callsFake((id: string) => {
|
|
632
|
+
currentActiveId = id;
|
|
633
|
+
return true;
|
|
634
|
+
});
|
|
635
|
+
mockProvider.getActivePerspectiveId.callsFake(() => currentActiveId);
|
|
636
|
+
mockProvider.getSavedLayout.returns(undefined);
|
|
637
|
+
|
|
638
|
+
const result = await restorer.restoreLayout(mockApp as never);
|
|
639
|
+
|
|
640
|
+
expect(result).to.be.false;
|
|
641
|
+
// onLayoutRestored should still be called
|
|
642
|
+
expect(mockProvider.onLayoutRestored.calledOnce).to.be.true;
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
describe('resetLayout', () => {
|
|
647
|
+
it('should clear layouts and legacy layout keys', async () => {
|
|
648
|
+
|
|
649
|
+
// Access private method
|
|
650
|
+
await (restorer as unknown as Record<string, () => Promise<void>>)['resetLayout']();
|
|
651
|
+
|
|
652
|
+
const setDataCalls = mockStorageService.setData.getCalls();
|
|
653
|
+
const layoutClear = setDataCalls.find((c: sinon.SinonSpyCall) => c.args[0] === 'layout' && c.args[1] === undefined);
|
|
654
|
+
const perspClear = setDataCalls.find((c: sinon.SinonSpyCall) => c.args[0] === PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] === undefined);
|
|
655
|
+
|
|
656
|
+
expect(layoutClear).to.not.be.undefined;
|
|
657
|
+
expect(perspClear).to.not.be.undefined;
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('should call provider.clearSavedLayouts', async () => {
|
|
661
|
+
|
|
662
|
+
await (restorer as unknown as Record<string, () => Promise<void>>)['resetLayout']();
|
|
663
|
+
|
|
664
|
+
expect(mockProvider.clearSavedLayouts.calledOnce).to.be.true;
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('should reload the window after reset', async () => {
|
|
668
|
+
|
|
669
|
+
await (restorer as unknown as Record<string, () => Promise<void>>)['resetLayout']();
|
|
670
|
+
|
|
671
|
+
expect(mockWindowService.reload.calledOnce).to.be.true;
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
describe('onLayoutRestored callback', () => {
|
|
676
|
+
it('should call onLayoutRestored on provider after successful restore', async () => {
|
|
677
|
+
|
|
678
|
+
const persisted: PersistedPerspectiveData = {
|
|
679
|
+
activePerspectiveId: 'my-persp',
|
|
680
|
+
layouts: {
|
|
681
|
+
'my-persp': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} })
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
685
|
+
|
|
686
|
+
let currentActiveId = 'default';
|
|
687
|
+
mockProvider.setActivePerspectiveId.callsFake((id: string) => {
|
|
688
|
+
currentActiveId = id;
|
|
689
|
+
return true;
|
|
690
|
+
});
|
|
691
|
+
mockProvider.getActivePerspectiveId.callsFake(() => currentActiveId);
|
|
692
|
+
|
|
693
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
694
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
695
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
696
|
+
);
|
|
697
|
+
return call?.args[1];
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
await restorer.restoreLayout(mockApp as never);
|
|
701
|
+
|
|
702
|
+
expect(mockProvider.onLayoutRestored.calledOnce).to.be.true;
|
|
703
|
+
expect(mockProvider.onLayoutRestored.calledWith('my-persp')).to.be.true;
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('should call onLayoutRestored even when no layout is restored', async () => {
|
|
707
|
+
|
|
708
|
+
mockStorageService.getData.resolves(undefined);
|
|
709
|
+
|
|
710
|
+
await restorer.restoreLayout(mockApp as never);
|
|
711
|
+
|
|
712
|
+
expect(mockProvider.onLayoutRestored.calledOnce).to.be.true;
|
|
713
|
+
expect(mockProvider.onLayoutRestored.calledWith('default')).to.be.true;
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('should call onLayoutRestored after layout is applied to shell', async () => {
|
|
717
|
+
|
|
718
|
+
const persisted: PersistedPerspectiveData = {
|
|
719
|
+
activePerspectiveId: 'active',
|
|
720
|
+
layouts: {
|
|
721
|
+
'active': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} })
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
mockStorageService.getData.withArgs(PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted);
|
|
725
|
+
|
|
726
|
+
let currentActiveId = 'default';
|
|
727
|
+
mockProvider.setActivePerspectiveId.callsFake((id: string) => {
|
|
728
|
+
currentActiveId = id;
|
|
729
|
+
return true;
|
|
730
|
+
});
|
|
731
|
+
mockProvider.getActivePerspectiveId.callsFake(() => currentActiveId);
|
|
732
|
+
|
|
733
|
+
mockProvider.getSavedLayout.callsFake((id: string) => {
|
|
734
|
+
const call = mockProvider.setSavedLayout.getCalls().find(
|
|
735
|
+
(c: sinon.SinonSpyCall) => c.args[0] === id
|
|
736
|
+
);
|
|
737
|
+
return call?.args[1];
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
await restorer.restoreLayout(mockApp as never);
|
|
741
|
+
|
|
742
|
+
// onLayoutRestored should be called after setLayoutData
|
|
743
|
+
expect(mockShell.setLayoutData.calledBefore(mockProvider.onLayoutRestored)).to.be.true;
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
});
|