@theia/core 1.74.0-next.9 → 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.
Files changed (209) hide show
  1. package/README.md +10 -10
  2. package/i18n/nls.cs.json +143 -16
  3. package/i18n/nls.de.json +143 -16
  4. package/i18n/nls.es.json +143 -16
  5. package/i18n/nls.fr.json +143 -16
  6. package/i18n/nls.hu.json +143 -16
  7. package/i18n/nls.it.json +143 -16
  8. package/i18n/nls.ja.json +143 -16
  9. package/i18n/nls.json +143 -16
  10. package/i18n/nls.ko.json +143 -16
  11. package/i18n/nls.pl.json +143 -16
  12. package/i18n/nls.pt-br.json +143 -16
  13. package/i18n/nls.ru.json +143 -16
  14. package/i18n/nls.tr.json +143 -16
  15. package/i18n/nls.zh-cn.json +143 -16
  16. package/i18n/nls.zh-tw.json +143 -16
  17. package/lib/browser/browser-clipboard-service.d.ts +3 -0
  18. package/lib/browser/browser-clipboard-service.d.ts.map +1 -1
  19. package/lib/browser/browser-clipboard-service.js +8 -1
  20. package/lib/browser/browser-clipboard-service.js.map +1 -1
  21. package/lib/browser/catalog.json +419 -41
  22. package/lib/browser/clipboard-service.d.ts +11 -0
  23. package/lib/browser/clipboard-service.d.ts.map +1 -1
  24. package/lib/browser/clipboard-service.js.map +1 -1
  25. package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
  26. package/lib/browser/common-frontend-contribution.js +7 -7
  27. package/lib/browser/common-frontend-contribution.js.map +1 -1
  28. package/lib/browser/frontend-application-module.d.ts.map +1 -1
  29. package/lib/browser/frontend-application-module.js +9 -0
  30. package/lib/browser/frontend-application-module.js.map +1 -1
  31. package/lib/browser/frontend-application.d.ts.map +1 -1
  32. package/lib/browser/frontend-application.js +12 -2
  33. package/lib/browser/frontend-application.js.map +1 -1
  34. package/lib/browser/index.d.ts +1 -0
  35. package/lib/browser/index.d.ts.map +1 -1
  36. package/lib/browser/index.js +1 -0
  37. package/lib/browser/index.js.map +1 -1
  38. package/lib/browser/keybinding.d.ts.map +1 -1
  39. package/lib/browser/keybinding.js +12 -7
  40. package/lib/browser/keybinding.js.map +1 -1
  41. package/lib/browser/perspective-service.d.ts +173 -0
  42. package/lib/browser/perspective-service.d.ts.map +1 -0
  43. package/lib/browser/perspective-service.js +484 -0
  44. package/lib/browser/perspective-service.js.map +1 -0
  45. package/lib/browser/perspective-service.spec.d.ts +2 -0
  46. package/lib/browser/perspective-service.spec.d.ts.map +1 -0
  47. package/lib/browser/perspective-service.spec.js +1806 -0
  48. package/lib/browser/perspective-service.spec.js.map +1 -0
  49. package/lib/browser/preferences/preference-contribution.d.ts.map +1 -1
  50. package/lib/browser/preferences/preference-contribution.js +9 -1
  51. package/lib/browser/preferences/preference-contribution.js.map +1 -1
  52. package/lib/browser/preferences/preference-service.spec.js +63 -0
  53. package/lib/browser/preferences/preference-service.spec.js.map +1 -1
  54. package/lib/browser/shell/application-shell.d.ts +7 -0
  55. package/lib/browser/shell/application-shell.d.ts.map +1 -1
  56. package/lib/browser/shell/application-shell.js +38 -28
  57. package/lib/browser/shell/application-shell.js.map +1 -1
  58. package/lib/browser/shell/shell-layout-restorer.d.ts +9 -0
  59. package/lib/browser/shell/shell-layout-restorer.d.ts.map +1 -1
  60. package/lib/browser/shell/shell-layout-restorer.js +124 -14
  61. package/lib/browser/shell/shell-layout-restorer.js.map +1 -1
  62. package/lib/browser/shell/shell-layout-restorer.spec.d.ts +2 -0
  63. package/lib/browser/shell/shell-layout-restorer.spec.d.ts.map +1 -0
  64. package/lib/browser/shell/shell-layout-restorer.spec.js +539 -0
  65. package/lib/browser/shell/shell-layout-restorer.spec.js.map +1 -0
  66. package/lib/browser/shell/view-contribution.d.ts +2 -0
  67. package/lib/browser/shell/view-contribution.d.ts.map +1 -1
  68. package/lib/browser/shell/view-contribution.js +5 -1
  69. package/lib/browser/shell/view-contribution.js.map +1 -1
  70. package/lib/browser/status-bar/status-bar.d.ts +2 -0
  71. package/lib/browser/status-bar/status-bar.d.ts.map +1 -1
  72. package/lib/browser/status-bar/status-bar.js +11 -3
  73. package/lib/browser/status-bar/status-bar.js.map +1 -1
  74. package/lib/browser/tree/tree-widget.d.ts +7 -33
  75. package/lib/browser/tree/tree-widget.d.ts.map +1 -1
  76. package/lib/browser/tree/tree-widget.js +7 -54
  77. package/lib/browser/tree/tree-widget.js.map +1 -1
  78. package/lib/browser/widgets/index.d.ts +1 -0
  79. package/lib/browser/widgets/index.d.ts.map +1 -1
  80. package/lib/browser/widgets/index.js +1 -0
  81. package/lib/browser/widgets/index.js.map +1 -1
  82. package/lib/browser/widgets/select-component.d.ts +2 -2
  83. package/lib/browser/widgets/select-component.d.ts.map +1 -1
  84. package/lib/browser/widgets/select-component.js +42 -12
  85. package/lib/browser/widgets/select-component.js.map +1 -1
  86. package/lib/browser/widgets/tab-bar-tracker.d.ts +23 -0
  87. package/lib/browser/widgets/tab-bar-tracker.d.ts.map +1 -0
  88. package/lib/browser/widgets/tab-bar-tracker.js +65 -0
  89. package/lib/browser/widgets/tab-bar-tracker.js.map +1 -0
  90. package/lib/browser/widgets/tab-bar-tracker.spec.d.ts +2 -0
  91. package/lib/browser/widgets/tab-bar-tracker.spec.d.ts.map +1 -0
  92. package/lib/browser/widgets/tab-bar-tracker.spec.js +109 -0
  93. package/lib/browser/widgets/tab-bar-tracker.spec.js.map +1 -0
  94. package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts +13 -2
  95. package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts.map +1 -1
  96. package/lib/browser-only/messaging/frontend-only-service-connection-provider.js +31 -3
  97. package/lib/browser-only/messaging/frontend-only-service-connection-provider.js.map +1 -1
  98. package/lib/common/preferences/index.d.ts +1 -0
  99. package/lib/common/preferences/index.d.ts.map +1 -1
  100. package/lib/common/preferences/index.js +1 -0
  101. package/lib/common/preferences/index.js.map +1 -1
  102. package/lib/common/preferences/preference-provider.d.ts +15 -0
  103. package/lib/common/preferences/preference-provider.d.ts.map +1 -1
  104. package/lib/common/preferences/preference-provider.js +21 -1
  105. package/lib/common/preferences/preference-provider.js.map +1 -1
  106. package/lib/common/preferences/preference-provider.spec.js +17 -0
  107. package/lib/common/preferences/preference-provider.spec.js.map +1 -1
  108. package/lib/common/preferences/preference-scope.d.ts +6 -1
  109. package/lib/common/preferences/preference-scope.d.ts.map +1 -1
  110. package/lib/common/preferences/preference-scope.js +5 -0
  111. package/lib/common/preferences/preference-scope.js.map +1 -1
  112. package/lib/common/preferences/preference-scope.spec.js +5 -3
  113. package/lib/common/preferences/preference-scope.spec.js.map +1 -1
  114. package/lib/common/preferences/preference-service.d.ts +19 -0
  115. package/lib/common/preferences/preference-service.d.ts.map +1 -1
  116. package/lib/common/preferences/preference-service.js +34 -5
  117. package/lib/common/preferences/preference-service.js.map +1 -1
  118. package/lib/common/preferences/session-preference-provider.d.ts +17 -0
  119. package/lib/common/preferences/session-preference-provider.d.ts.map +1 -0
  120. package/lib/common/preferences/session-preference-provider.js +77 -0
  121. package/lib/common/preferences/session-preference-provider.js.map +1 -0
  122. package/lib/common/preferences/session-preference-provider.spec.d.ts +2 -0
  123. package/lib/common/preferences/session-preference-provider.spec.d.ts.map +1 -0
  124. package/lib/common/preferences/session-preference-provider.spec.js +71 -0
  125. package/lib/common/preferences/session-preference-provider.spec.js.map +1 -0
  126. package/lib/electron-browser/electron-clipboard-service.d.ts +3 -0
  127. package/lib/electron-browser/electron-clipboard-service.d.ts.map +1 -1
  128. package/lib/electron-browser/electron-clipboard-service.js +6 -0
  129. package/lib/electron-browser/electron-clipboard-service.js.map +1 -1
  130. package/lib/electron-browser/preload.d.ts.map +1 -1
  131. package/lib/electron-browser/preload.js +2 -1
  132. package/lib/electron-browser/preload.js.map +1 -1
  133. package/lib/electron-common/electron-api.d.ts +2 -0
  134. package/lib/electron-common/electron-api.d.ts.map +1 -1
  135. package/lib/electron-common/electron-api.js +2 -1
  136. package/lib/electron-common/electron-api.js.map +1 -1
  137. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  138. package/lib/electron-main/electron-api-main.js +43 -0
  139. package/lib/electron-main/electron-api-main.js.map +1 -1
  140. package/lib/electron-main/electron-main-application.d.ts.map +1 -1
  141. package/lib/electron-main/electron-main-application.js +3 -0
  142. package/lib/electron-main/electron-main-application.js.map +1 -1
  143. package/lib/electron-node/hosting/electron-backend-hosting-module.d.ts.map +1 -1
  144. package/lib/electron-node/hosting/electron-backend-hosting-module.js +7 -0
  145. package/lib/electron-node/hosting/electron-backend-hosting-module.js.map +1 -1
  146. package/lib/electron-node/hosting/electron-backend-hosting-module.spec.d.ts +2 -0
  147. package/lib/electron-node/hosting/electron-backend-hosting-module.spec.d.ts.map +1 -0
  148. package/lib/electron-node/hosting/electron-backend-hosting-module.spec.js +39 -0
  149. package/lib/electron-node/hosting/electron-backend-hosting-module.spec.js.map +1 -0
  150. package/lib/node/hosting/backend-hosting-module.d.ts.map +1 -1
  151. package/lib/node/hosting/backend-hosting-module.js +4 -1
  152. package/lib/node/hosting/backend-hosting-module.js.map +1 -1
  153. package/lib/node/hosting/browser-connection-token.d.ts +29 -1
  154. package/lib/node/hosting/browser-connection-token.d.ts.map +1 -1
  155. package/lib/node/hosting/browser-connection-token.js +25 -1
  156. package/lib/node/hosting/browser-connection-token.js.map +1 -1
  157. package/lib/node/hosting/browser-connection-token.spec.js +70 -0
  158. package/lib/node/hosting/browser-connection-token.spec.js.map +1 -1
  159. package/lib/node/index.d.ts +1 -0
  160. package/lib/node/index.d.ts.map +1 -1
  161. package/lib/node/index.js +3 -1
  162. package/lib/node/index.js.map +1 -1
  163. package/package.json +11 -11
  164. package/src/browser/browser-clipboard-service.ts +7 -1
  165. package/src/browser/clipboard-service.ts +11 -0
  166. package/src/browser/common-frontend-contribution.ts +7 -7
  167. package/src/browser/frontend-application-module.ts +11 -1
  168. package/src/browser/frontend-application.ts +12 -4
  169. package/src/browser/index.ts +1 -0
  170. package/src/browser/keybinding.ts +12 -7
  171. package/src/browser/perspective-service.spec.ts +2270 -0
  172. package/src/browser/perspective-service.ts +625 -0
  173. package/src/browser/preferences/preference-contribution.ts +9 -1
  174. package/src/browser/preferences/preference-service.spec.ts +72 -0
  175. package/src/browser/shell/application-shell.ts +41 -26
  176. package/src/browser/shell/shell-layout-restorer.spec.ts +746 -0
  177. package/src/browser/shell/shell-layout-restorer.ts +134 -13
  178. package/src/browser/shell/view-contribution.ts +6 -1
  179. package/src/browser/status-bar/status-bar.tsx +13 -3
  180. package/src/browser/style/select-component.css +16 -0
  181. package/src/browser/style/sidepanel.css +5 -0
  182. package/src/browser/style/status-bar.css +1 -1
  183. package/src/browser/tree/tree-widget.tsx +12 -78
  184. package/src/browser/widgets/index.ts +1 -0
  185. package/src/browser/widgets/select-component.tsx +41 -12
  186. package/src/browser/widgets/tab-bar-tracker.spec.ts +121 -0
  187. package/src/browser/widgets/tab-bar-tracker.ts +68 -0
  188. package/src/browser-only/messaging/frontend-only-service-connection-provider.ts +32 -3
  189. package/src/common/i18n/nls.metadata.json +4149 -888
  190. package/src/common/preferences/index.ts +1 -0
  191. package/src/common/preferences/preference-provider.spec.ts +17 -0
  192. package/src/common/preferences/preference-provider.ts +22 -1
  193. package/src/common/preferences/preference-scope.spec.ts +5 -3
  194. package/src/common/preferences/preference-scope.ts +6 -1
  195. package/src/common/preferences/preference-service.ts +39 -5
  196. package/src/common/preferences/session-preference-provider.spec.ts +78 -0
  197. package/src/common/preferences/session-preference-provider.ts +69 -0
  198. package/src/electron-browser/electron-clipboard-service.ts +5 -0
  199. package/src/electron-browser/menu/electron-menu-style.css +3 -1
  200. package/src/electron-browser/preload.ts +5 -2
  201. package/src/electron-common/electron-api.ts +4 -0
  202. package/src/electron-main/electron-api-main.ts +56 -3
  203. package/src/electron-main/electron-main-application.ts +3 -0
  204. package/src/electron-node/hosting/electron-backend-hosting-module.spec.ts +42 -0
  205. package/src/electron-node/hosting/electron-backend-hosting-module.ts +8 -0
  206. package/src/node/hosting/backend-hosting-module.ts +5 -2
  207. package/src/node/hosting/browser-connection-token.spec.ts +92 -0
  208. package/src/node/hosting/browser-connection-token.ts +45 -1
  209. 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
+ });