UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

539 lines • 31.7 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2026 EclipseSource GmbH. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); const jsdom_1 = require("../test/jsdom"); const disableJSDOM = (0, jsdom_1.enableJSDOM)(); const chai_1 = require("chai"); const sinon = require("sinon"); const shell_layout_restorer_1 = require("./shell-layout-restorer"); disableJSDOM(); describe('ShellLayoutRestorer - Perspective Support', () => { let restorer; let mockStorageService; let mockLogger; let mockWidgetManager; let mockShell; let mockApp; let mockProvider; let mockTransformations; let mockMigrations; let mockWindowService; let mockThemeService; let toTearDown; beforeEach(() => { toTearDown = (0, jsdom_1.enableJSDOM)(); mockStorageService = { setData: sinon.stub().resolves(), getData: sinon.stub().resolves(undefined) }; mockLogger = { info: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }; mockWidgetManager = { getDescription: sinon.stub().returns(undefined) }; mockShell = { getLayoutData: sinon.stub().returns({ version: 999, mainPanel: {}, bottomPanel: {} }), setLayoutData: sinon.stub().resolves() }; mockApp = { shell: mockShell }; mockProvider = { getActivePerspectiveId: sinon.stub().returns('default'), getSavedPerspectiveIds: sinon.stub().returns([]), getSavedLayout: sinon.stub().returns(undefined), setSavedLayout: sinon.stub(), setActivePerspectiveId: sinon.stub().returns(true), clearSavedLayouts: sinon.stub(), onLayoutRestored: sinon.stub(), defaultPerspectiveId: 'default' }; mockTransformations = { getContributions: sinon.stub().returns([]) }; mockMigrations = { getContributions: sinon.stub().returns([]) }; mockWindowService = { isSafeToShutDown: sinon.stub().resolves(true), reload: sinon.stub() }; mockThemeService = { reset: sinon.stub() }; restorer = new shell_layout_restorer_1.ShellLayoutRestorer(mockWidgetManager, mockLogger, mockStorageService); restorer['perspectiveService'] = mockProvider; restorer['transformations'] = mockTransformations; restorer['migrations'] = mockMigrations; restorer['windowService'] = mockWindowService; restorer['themeService'] = mockThemeService; }); afterEach(() => { sinon.restore(); toTearDown(); }); describe('storePerspectiveLayouts', () => { it('should read current shell layout as the active perspective layout', () => { mockProvider.getActivePerspectiveId.returns('persp-a'); mockProvider.getSavedPerspectiveIds.returns([]); const currentLayout = { version: 999, mainPanel: { items: ['editor1'] }, bottomPanel: {} }; mockShell.getLayoutData.returns(currentLayout); restorer.storeLayout(mockApp); const perspCall = mockStorageService.setData.getCalls().find((c) => c.args[0] === shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] !== undefined); (0, chai_1.expect)(perspCall).to.not.be.undefined; const data = perspCall.args[1]; (0, chai_1.expect)(data.activePerspectiveId).to.equal('persp-a'); (0, chai_1.expect)(data.layouts['persp-a']).to.be.a('string'); // Verify it contains the current shell layout content const parsed = JSON.parse(data.layouts['persp-a']); (0, chai_1.expect)(parsed.mainPanel.items).to.deep.equal(['editor1']); }); it('should deflate all inactive perspective layouts from provider', () => { mockProvider.getActivePerspectiveId.returns('active'); mockProvider.getSavedPerspectiveIds.returns(['active', 'inactive1', 'inactive2']); const inactiveLayout1 = { version: 999, mainPanel: { items: ['w1'] }, bottomPanel: {} }; const inactiveLayout2 = { version: 999, mainPanel: { items: ['w2'] }, bottomPanel: {} }; mockProvider.getSavedLayout.withArgs('inactive1').returns(inactiveLayout1); mockProvider.getSavedLayout.withArgs('inactive2').returns(inactiveLayout2); mockProvider.getSavedLayout.withArgs('active').returns(undefined); restorer.storeLayout(mockApp); const [, data] = mockStorageService.setData.firstCall.args; (0, chai_1.expect)(Object.keys(data.layouts)).to.have.lengthOf(3); (0, chai_1.expect)(data.layouts).to.have.property('active'); (0, chai_1.expect)(data.layouts).to.have.property('inactive1'); (0, chai_1.expect)(data.layouts).to.have.property('inactive2'); }); it('should use shell layout for active perspective, not provider', () => { mockProvider.getActivePerspectiveId.returns('active'); mockProvider.getSavedPerspectiveIds.returns(['active']); // Provider has stale data for the active perspective const staleLayout = { version: 999, mainPanel: { items: ['stale'] }, bottomPanel: {} }; mockProvider.getSavedLayout.withArgs('active').returns(staleLayout); // Shell has the current data const currentLayout = { version: 999, mainPanel: { items: ['current'] }, bottomPanel: {} }; mockShell.getLayoutData.returns(currentLayout); restorer.storeLayout(mockApp); const [, data] = mockStorageService.setData.firstCall.args; const parsed = JSON.parse(data.layouts['active']); // Should use shell data, not provider data (0, chai_1.expect)(parsed.mainPanel.items).to.deep.equal(['current']); }); it('should write to legacy key when only default perspective is used', () => { mockProvider.getActivePerspectiveId.returns('default'); mockProvider.getSavedPerspectiveIds.returns([]); restorer.storeLayout(mockApp); // Should write to legacy key for backward compatibility const legacyCall = mockStorageService.setData.getCalls().find((c) => c.args[0] === 'layout'); (0, chai_1.expect)(legacyCall).to.not.be.undefined; // Should clear the perspective key const perspClear = mockStorageService.setData.getCalls().find((c) => c.args[0] === shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] === undefined); (0, chai_1.expect)(perspClear).to.not.be.undefined; }); it('should write to perspective key when multiple perspectives are used', () => { mockProvider.getActivePerspectiveId.returns('persp-a'); mockProvider.getSavedPerspectiveIds.returns(['persp-a', 'persp-b']); const inactiveLayout = { version: 999, mainPanel: { items: ['b'] }, bottomPanel: {} }; mockProvider.getSavedLayout.withArgs('persp-b').returns(inactiveLayout); mockProvider.getSavedLayout.withArgs('persp-a').returns(undefined); restorer.storeLayout(mockApp); const perspCall = mockStorageService.setData.getCalls().find((c) => c.args[0] === shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] !== undefined); (0, chai_1.expect)(perspCall).to.not.be.undefined; // Should clear the legacy key const legacyClear = mockStorageService.setData.getCalls().find((c) => c.args[0] === 'layout' && c.args[1] === undefined); (0, chai_1.expect)(legacyClear).to.not.be.undefined; }); }); describe('restorePerspectiveLayouts', () => { it('should inflate all layouts and push to provider', async () => { const persisted = { activePerspectiveId: 'persp-a', layouts: { 'persp-a': JSON.stringify({ version: 999, mainPanel: { items: ['a'] }, bottomPanel: {} }), 'persp-b': JSON.stringify({ version: 999, mainPanel: { items: ['b'] }, bottomPanel: {} }) } }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); // Return the inflated layout for the active persp when asked mockProvider.getSavedLayout.callsFake((id) => { if (id === 'persp-a') { // After setSavedLayout is called, return the layout const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === 'persp-a'); return call?.args[1]; } return undefined; }); const result = await restorer.restoreLayout(mockApp); (0, chai_1.expect)(result).to.be.true; (0, chai_1.expect)(mockProvider.setSavedLayout.callCount).to.equal(2); // Verify persp-a was set const setACall = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === 'persp-a'); (0, chai_1.expect)(setACall).to.not.be.undefined; // Verify persp-b was set const setBCall = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === 'persp-b'); (0, chai_1.expect)(setBCall).to.not.be.undefined; }); it('should set active perspective ID on provider', async () => { const persisted = { activePerspectiveId: 'my-persp', layouts: { 'my-persp': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }) } }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); await restorer.restoreLayout(mockApp); (0, chai_1.expect)(mockProvider.setActivePerspectiveId.calledWith('my-persp')).to.be.true; }); it('should apply the active layout to the shell', async () => { const layoutData = { version: 999, mainPanel: { items: ['active-item'] }, bottomPanel: {} }; const persisted = { activePerspectiveId: 'active', layouts: { 'active': JSON.stringify(layoutData) } }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); await restorer.restoreLayout(mockApp); (0, chai_1.expect)(mockShell.setLayoutData.calledOnce).to.be.true; const applied = mockShell.setLayoutData.firstCall.args[0]; (0, chai_1.expect)(applied.mainPanel.items).to.deep.equal(['active-item']); }); it('should apply transformations to all inflated layouts, not just the active one', async () => { const transformer = { transformLayoutOnRestore: sinon.stub() }; mockTransformations.getContributions.returns([transformer]); const persisted = { activePerspectiveId: 'persp-a', layouts: { 'persp-a': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }), 'persp-b': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }) } }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); await restorer.restoreLayout(mockApp); // Transformer should be called for both layouts (0, chai_1.expect)(transformer.transformLayoutOnRestore.callCount).to.equal(2); }); it('should fall back to provider active ID when persisted ID is not registered', async () => { const persisted = { activePerspectiveId: 'removed-persp', layouts: { 'removed-persp': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }), 'default': JSON.stringify({ version: 999, mainPanel: { items: ['default-w'] }, bottomPanel: {} }) } }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); mockProvider.setActivePerspectiveId.returns(false); mockProvider.getActivePerspectiveId.returns('default'); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); const result = await restorer.restoreLayout(mockApp); (0, chai_1.expect)(result).to.be.true; // Should apply 'default' layout to shell, not 'removed-persp' const applied = mockShell.setLayoutData.firstCall.args[0]; (0, chai_1.expect)(applied.mainPanel.items).to.deep.equal(['default-w']); // onLayoutRestored should be called with 'default' (0, chai_1.expect)(mockProvider.onLayoutRestored.calledWith('default')).to.be.true; }); it('should return false when no perspective data and no legacy data exists', async () => { mockStorageService.getData.resolves(undefined); const result = await restorer.restoreLayout(mockApp); (0, chai_1.expect)(result).to.be.false; }); it('should warn when a perspective layout fails to inflate', async () => { const persisted = { activePerspectiveId: 'good', layouts: { 'good': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }), 'bad': 'invalid-json{' } }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); await restorer.restoreLayout(mockApp); // 'good' should be inflated successfully const goodCall = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === 'good'); (0, chai_1.expect)(goodCall).to.not.be.undefined; // Should warn about 'bad' (0, chai_1.expect)(mockLogger.warn.called).to.be.true; const warnCall = mockLogger.warn.getCalls().find((c) => c.args[0].includes('bad')); (0, chai_1.expect)(warnCall).to.not.be.undefined; }); }); describe('legacy migration', () => { it('should fall back to legacy key when perspective-layouts is absent', async () => { const legacyLayout = JSON.stringify({ version: 999, mainPanel: { items: ['legacy'] }, bottomPanel: {} }); mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(undefined); mockStorageService.getData.withArgs('layout').resolves(legacyLayout); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); const result = await restorer.restoreLayout(mockApp); (0, chai_1.expect)(result).to.be.true; // Should push layout as 'default' perspective (0, chai_1.expect)(mockProvider.setSavedLayout.calledOnce).to.be.true; (0, chai_1.expect)(mockProvider.setSavedLayout.firstCall.args[0]).to.equal('default'); // Should set active to 'default' (0, chai_1.expect)(mockProvider.setActivePerspectiveId.calledWith('default')).to.be.true; }); it('should keep the legacy key after a successful migration', async () => { const legacyLayout = JSON.stringify({ version: 999, mainPanel: { items: ['legacy'] }, bottomPanel: {} }); mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(undefined); mockStorageService.getData.withArgs('layout').resolves(legacyLayout); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); await restorer.restoreLayout(mockApp); // Restoring must not touch the key: it is rewritten or cleared by the next `storeLayout`, // so a crash before then still finds the layout where it was. const legacyWrite = mockStorageService.setData.getCalls().find((c) => c.args[0] === 'layout'); (0, chai_1.expect)(legacyWrite).to.be.undefined; }); it('should preserve legacy key when inflate fails', async () => { mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(undefined); mockStorageService.getData.withArgs('layout').resolves('not-valid-json{'); const result = await restorer.restoreLayout(mockApp); (0, chai_1.expect)(result).to.be.false; // Legacy key should NOT be cleared const legacyClear = mockStorageService.setData.getCalls().find((c) => c.args[0] === 'layout' && c.args[1] === undefined); (0, chai_1.expect)(legacyClear).to.be.undefined; // No layout should have been saved (0, chai_1.expect)(mockProvider.setSavedLayout.called).to.be.false; // Should warn about the failure (0, chai_1.expect)(mockLogger.warn.called).to.be.true; }); it('should apply migrated legacy layout to shell', async () => { const legacyLayout = JSON.stringify({ version: 999, mainPanel: { items: ['legacy-w'] }, bottomPanel: {} }); mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(undefined); mockStorageService.getData.withArgs('layout').resolves(legacyLayout); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); await restorer.restoreLayout(mockApp); (0, chai_1.expect)(mockShell.setLayoutData.calledOnce).to.be.true; }); }); describe('storePerspectiveLayouts - error isolation', () => { it('should persist other perspectives when one inactive perspective deflate fails', () => { mockProvider.getActivePerspectiveId.returns('active'); mockProvider.getSavedPerspectiveIds.returns(['active', 'good', 'bad']); const goodLayout = { version: 999, mainPanel: { items: ['good'] }, bottomPanel: {} }; // Create a layout with a circular reference that will cause JSON.stringify to throw const badLayout = { version: 999, mainPanel: {}, bottomPanel: {} }; badLayout['self'] = badLayout; mockProvider.getSavedLayout.withArgs('good').returns(goodLayout); mockProvider.getSavedLayout.withArgs('bad').returns(badLayout); mockProvider.getSavedLayout.withArgs('active').returns(undefined); restorer.storeLayout(mockApp); // Multiple perspectives => writes to PERSPECTIVE_LAYOUTS_STORAGE_KEY and clears legacy const perspCall = mockStorageService.setData.getCalls().find((c) => c.args[0] === shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] !== undefined); (0, chai_1.expect)(perspCall).to.not.be.undefined; const data = perspCall.args[1]; (0, chai_1.expect)(data.layouts).to.have.property('active'); (0, chai_1.expect)(data.layouts).to.have.property('good'); (0, chai_1.expect)(data.layouts).to.not.have.property('bad'); // Should warn about 'bad' (0, chai_1.expect)(mockLogger.warn.called).to.be.true; const warnCall = mockLogger.warn.getCalls().find((c) => c.args[0].includes('bad')); (0, chai_1.expect)(warnCall).to.not.be.undefined; }); it('should persist inactive perspectives when active perspective deflate fails', () => { mockProvider.getActivePerspectiveId.returns('active'); mockProvider.getSavedPerspectiveIds.returns(['active', 'inactive']); // Make the shell return a circular structure for the active perspective const circularLayout = { version: 999, mainPanel: {}, bottomPanel: {} }; circularLayout['self'] = circularLayout; mockShell.getLayoutData.returns(circularLayout); const inactiveLayout = { version: 999, mainPanel: { items: ['inactive'] }, bottomPanel: {} }; mockProvider.getSavedLayout.withArgs('inactive').returns(inactiveLayout); restorer.storeLayout(mockApp); // Multiple perspectives (active + inactive) => writes to PERSPECTIVE_LAYOUTS_STORAGE_KEY const perspCall = mockStorageService.setData.getCalls().find((c) => c.args[0] === shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] !== undefined); (0, chai_1.expect)(perspCall).to.not.be.undefined; const data = perspCall.args[1]; (0, chai_1.expect)(data.layouts).to.not.have.property('active'); (0, chai_1.expect)(data.layouts).to.have.property('inactive'); (0, chai_1.expect)(mockLogger.warn.called).to.be.true; }); it('should clear storage key when setData rejects (multi-perspective)', async () => { mockProvider.getActivePerspectiveId.returns('persp-a'); mockProvider.getSavedPerspectiveIds.returns(['persp-a', 'persp-b']); const layout = { version: 999, mainPanel: {}, bottomPanel: {} }; mockProvider.getSavedLayout.withArgs('persp-b').returns(layout); // The perspective-layouts setData call rejects only the first time (when data is present) let perspCallCount = 0; mockStorageService.setData.callsFake((key, data) => { if (key === shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY && data !== undefined) { perspCallCount++; return Promise.reject(new Error('storage error')); } return Promise.resolve(); }); restorer.storeLayout(mockApp); // Allow the .catch() handler to execute await new Promise(resolve => setTimeout(resolve, 0)); (0, chai_1.expect)(perspCallCount).to.equal(1); // Should have attempted to clear the perspective key const clearCall = mockStorageService.setData.getCalls().find((c) => c.args[0] === shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] === undefined); (0, chai_1.expect)(clearCall).to.not.be.undefined; (0, chai_1.expect)(mockLogger.error.called).to.be.true; }); }); describe('restorePerspectiveLayouts - desync fallback', () => { it('should fall back to default perspective when active perspective has no saved layout', async () => { const persisted = { activePerspectiveId: 'custom', layouts: { 'default': JSON.stringify({ version: 999, mainPanel: { items: ['default-w'] }, bottomPanel: {} }) } }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); // 'custom' is a registered perspective, but has no layout entry mockProvider.setActivePerspectiveId.returns(true); // Track calls to setActivePerspectiveId to simulate state changes let currentActiveId = 'default'; mockProvider.setActivePerspectiveId.callsFake((id) => { currentActiveId = id; return true; }); mockProvider.getActivePerspectiveId.callsFake(() => currentActiveId); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); const result = await restorer.restoreLayout(mockApp); (0, chai_1.expect)(result).to.be.true; // Should have fallen back to default (0, chai_1.expect)(mockLogger.warn.called).to.be.true; const warnCall = mockLogger.warn.getCalls().find((c) => c.args[0].includes('custom')); (0, chai_1.expect)(warnCall).to.not.be.undefined; // Should have called onLayoutRestored with default (0, chai_1.expect)(mockProvider.onLayoutRestored.calledWith('default')).to.be.true; }); it('should call onLayoutRestored even when falling through to no layout', async () => { const persisted = { activePerspectiveId: 'custom', layouts: {} }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); let currentActiveId = 'default'; mockProvider.setActivePerspectiveId.callsFake((id) => { currentActiveId = id; return true; }); mockProvider.getActivePerspectiveId.callsFake(() => currentActiveId); mockProvider.getSavedLayout.returns(undefined); const result = await restorer.restoreLayout(mockApp); (0, chai_1.expect)(result).to.be.false; // onLayoutRestored should still be called (0, chai_1.expect)(mockProvider.onLayoutRestored.calledOnce).to.be.true; }); }); describe('resetLayout', () => { it('should clear layouts and legacy layout keys', async () => { // Access private method await restorer['resetLayout'](); const setDataCalls = mockStorageService.setData.getCalls(); const layoutClear = setDataCalls.find((c) => c.args[0] === 'layout' && c.args[1] === undefined); const perspClear = setDataCalls.find((c) => c.args[0] === shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY && c.args[1] === undefined); (0, chai_1.expect)(layoutClear).to.not.be.undefined; (0, chai_1.expect)(perspClear).to.not.be.undefined; }); it('should call provider.clearSavedLayouts', async () => { await restorer['resetLayout'](); (0, chai_1.expect)(mockProvider.clearSavedLayouts.calledOnce).to.be.true; }); it('should reload the window after reset', async () => { await restorer['resetLayout'](); (0, chai_1.expect)(mockWindowService.reload.calledOnce).to.be.true; }); }); describe('onLayoutRestored callback', () => { it('should call onLayoutRestored on provider after successful restore', async () => { const persisted = { activePerspectiveId: 'my-persp', layouts: { 'my-persp': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }) } }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); let currentActiveId = 'default'; mockProvider.setActivePerspectiveId.callsFake((id) => { currentActiveId = id; return true; }); mockProvider.getActivePerspectiveId.callsFake(() => currentActiveId); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); await restorer.restoreLayout(mockApp); (0, chai_1.expect)(mockProvider.onLayoutRestored.calledOnce).to.be.true; (0, chai_1.expect)(mockProvider.onLayoutRestored.calledWith('my-persp')).to.be.true; }); it('should call onLayoutRestored even when no layout is restored', async () => { mockStorageService.getData.resolves(undefined); await restorer.restoreLayout(mockApp); (0, chai_1.expect)(mockProvider.onLayoutRestored.calledOnce).to.be.true; (0, chai_1.expect)(mockProvider.onLayoutRestored.calledWith('default')).to.be.true; }); it('should call onLayoutRestored after layout is applied to shell', async () => { const persisted = { activePerspectiveId: 'active', layouts: { 'active': JSON.stringify({ version: 999, mainPanel: {}, bottomPanel: {} }) } }; mockStorageService.getData.withArgs(shell_layout_restorer_1.PERSPECTIVE_LAYOUTS_STORAGE_KEY).resolves(persisted); let currentActiveId = 'default'; mockProvider.setActivePerspectiveId.callsFake((id) => { currentActiveId = id; return true; }); mockProvider.getActivePerspectiveId.callsFake(() => currentActiveId); mockProvider.getSavedLayout.callsFake((id) => { const call = mockProvider.setSavedLayout.getCalls().find((c) => c.args[0] === id); return call?.args[1]; }); await restorer.restoreLayout(mockApp); // onLayoutRestored should be called after setLayoutData (0, chai_1.expect)(mockShell.setLayoutData.calledBefore(mockProvider.onLayoutRestored)).to.be.true; }); }); }); //# sourceMappingURL=shell-layout-restorer.spec.js.map