From 2e1bcbc4c273aba6880efe1d59693082feac4f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 18 Mar 2026 17:18:23 +0100 Subject: [PATCH 01/13] Refactor launchApp to avoid `app.app` callsites --- e2e/helpers/connect.ts | 14 +++++++++----- e2e/scripts/open-connect.ts | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/e2e/helpers/connect.ts b/e2e/helpers/connect.ts index 6006dc8c67004..e399634db7fc8 100644 --- a/e2e/helpers/connect.ts +++ b/e2e/helpers/connect.ts @@ -39,7 +39,7 @@ export async function launchApp(homeDir: string) { ); const executablePath = requireFromApp('electron'); - const app = await electron.launch({ + const electronApp = await electron.launch({ executablePath, args: [connectAppDir, '--insecure'], env: { @@ -51,12 +51,16 @@ export async function launchApp(homeDir: string) { }); try { - const page = await app.firstWindow(); + const page = await electronApp.firstWindow(); await page.waitForLoadState('domcontentloaded'); - return { app, page, [Symbol.asyncDispose]: async () => app.close() }; + return { + electronApp, + page, + [Symbol.asyncDispose]: async () => electronApp.close(), + }; } catch (err) { - await app.close(); + await electronApp.close(); throw err; } } @@ -104,7 +108,7 @@ export const test = base.extend<{ await login(launchedApp.page); } await use({ - electronApp: launchedApp.app, + electronApp: launchedApp.electronApp, page: launchedApp.page, appConfigPath, }); diff --git a/e2e/scripts/open-connect.ts b/e2e/scripts/open-connect.ts index ef6a975005755..b13e170c2079d 100644 --- a/e2e/scripts/open-connect.ts +++ b/e2e/scripts/open-connect.ts @@ -45,5 +45,5 @@ await login(launched.page); info('Teleport Connect opened and authenticated'); info('close the app window or press Ctrl+C to exit'); -await new Promise(resolve => launched.app.once('close', resolve)); +await new Promise(resolve => launched.electronApp.once('close', resolve)); info('Teleport Connect closed'); From 5ea7eeff1b3a373be35653c98c0abb4e5942267f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 18 Mar 2026 12:40:33 +0100 Subject: [PATCH 02/13] Add tests for state restoration --- e2e/tests/connect/stateRestoration.spec.ts | 209 +++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 e2e/tests/connect/stateRestoration.spec.ts diff --git a/e2e/tests/connect/stateRestoration.spec.ts b/e2e/tests/connect/stateRestoration.spec.ts new file mode 100644 index 0000000000000..36e54cb6bf55b --- /dev/null +++ b/e2e/tests/connect/stateRestoration.spec.ts @@ -0,0 +1,209 @@ +/** + * Teleport + * Copyright (C) 2026 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { + expect, + initializeDataDir, + launchApp, + login, + test, + withDefaultAppConfig, +} from '@gravitational/e2e/helpers/connect'; + +// These tests manage the app lifecycle manually (multiple launches/closes), so they do not use the +// `app` fixture. +test.describe('state restoration from disk', () => { + let tempPath: string; + + test.beforeEach(async () => { + tempPath = await fs.mkdtemp(path.join(os.tmpdir(), 'connect-e2e-state-')); + await initializeDataDir(tempPath, withDefaultAppConfig({})); + }); + + test.afterEach(async () => { + await fs.rm(tempPath, { recursive: true, force: true }); + }); + + test('relaunch restores tabs', async () => { + // Login and create extra tabs. + { + await using app = await launchApp(tempPath); + const { page } = app; + await login(page); + + // Open a terminal tab so there are 2 tabs (cluster + terminal). + await page.getByTitle('Additional Actions').click(); + await page.getByText('Open new terminal').click(); + await expect( + page.getByRole('textbox', { name: 'Terminal input' }) + ).toBeVisible(); + } + + // Relaunch – the app should offer to restore the terminal tab. + { + await using app = await launchApp(tempPath); + const { page } = app; + await expect(page.getByText('Reopen previous session')).toBeVisible(); + await page.getByRole('button', { name: 'Reopen' }).click(); + + // Both tabs should be restored. + await expect( + page.locator('[role="tab"][data-doc-kind="doc.cluster"]') + ).toBeVisible(); + await expect( + page.locator('[role="tab"][data-doc-kind="doc.terminal_shell"]') + ).toBeVisible(); + } + }); + + test('missing state files do not crash the app', async () => { + const userDataDir = path.join(tempPath, 'userData'); + const appStatePath = path.join(userDataDir, 'app_state.json'); + const tshHomePath = path.join(tempPath, 'home', '.tsh'); + + // Login to create state files on disk. + { + await using app = await launchApp(tempPath); + await login(app.page); + } + + // Remove app_state.json (keep tsh dir) – the app should not crash. + { + await fs.rm(appStatePath); + await using app = await launchApp(tempPath); + + // Without app_state.json, the app has no saved rootClusterUri, so no workspace is activated + // and the cluster connect panel prompts the user to pick one. + await expect( + app.page.getByText('Log in to a cluster to use Teleport Connect.') + ).toBeVisible(); + } + + // Remove the tsh home directory – the app should not crash. + { + await fs.rm(tshHomePath, { recursive: true }); + await using app = await launchApp(tempPath); + + // With no tsh dir, no cluster can be connected. + await expect(app.page.getByText('Connect a Cluster')).toBeVisible(); + } + }); + + test('logout clears previous tabs', async () => { + await using app = await launchApp(tempPath); + const { page } = app; + await login(page); + + // Open a terminal tab. + await page.getByTitle('Additional Actions').click(); + await page.getByText('Open new terminal').click(); + await expect( + page.getByRole('textbox', { name: 'Terminal input' }) + ).toBeVisible(); + + // Logout. + await page.getByTitle(/Open Profiles/).click(); + await page.getByTitle(/Log out/).click(); + await expect( + page.getByText('Are you sure you want to log out?') + ).toBeVisible(); + await page.getByRole('button', { name: 'Log Out', exact: true }).click(); + await expect(page.getByText('Connect a Cluster')).toBeVisible(); + + // Login to the same cluster again. + await login(page); + + // No restore dialog should appear – logout clears previous tabs. + // Only the default cluster tab should be open. + const clusterTab = page.locator( + '[role="tab"][data-doc-kind="doc.cluster"]' + ); + await expect(clusterTab).toHaveCount(1); + await expect( + page.locator('[role="tab"][data-doc-kind="doc.terminal_shell"]') + ).toHaveCount(0); + }); + + test('identical workspace shape does not trigger restore dialog', async () => { + // Login, then replace the default cluster tab with a new one (same shape). + { + await using app = await launchApp(tempPath); + const { page } = app; + await login(page); + + const clusterTab = page.locator( + '[role="tab"][data-doc-kind="doc.cluster"]' + ); + await expect(clusterTab).toBeVisible(); + + // Close the existing cluster tab. + await clusterTab.locator('.close').click(); + // Open a new cluster tab via the "+" button. + await page.getByTitle(/New Tab/).click(); + await expect(clusterTab).toBeVisible(); + } + + // Relaunch – no restore dialog since the workspace has the same shape. + { + await using app = await launchApp(tempPath); + const { page } = app; + + const clusterTab = page.locator( + '[role="tab"][data-doc-kind="doc.cluster"]' + ); + await expect(clusterTab).toHaveCount(1); + await expect(page.getByText('Reopen previous session')).not.toBeVisible(); + } + }); + + test('window remembers size and position after restart', async () => { + // Launch the app and resize the window. + { + await using app = await launchApp(tempPath); + const { page } = app; + await expect(page.getByText('Connect a Cluster')).toBeVisible(); + + const targetBounds = { x: 100, y: 100, width: 900, height: 600 }; + await app.electronApp.evaluate(({ BrowserWindow }, bounds) => { + const win = BrowserWindow.getAllWindows()[0]; + win.setBounds(bounds); + }, targetBounds); + } + + // Relaunch – the window should restore to the same size & position. + { + await using app = await launchApp(tempPath); + + // Wait for the app to fully initialize before reading bounds and closing, otherwise the + // app may close before the renderer acks the initial cluster store message, causing a + // "Failed to receive message acknowledgement from the renderer" error dialog. + await expect(app.page.getByText('Connect a Cluster')).toBeVisible(); + + const bounds = await app.electronApp.evaluate(({ BrowserWindow }) => { + const win = BrowserWindow.getAllWindows()[0]; + return win.getNormalBounds(); + }); + + expect(bounds).toEqual({ x: 100, y: 100, width: 900, height: 600 }); + } + }); +}); From f443df4a653007900099c7f7dc89f1ffe8a0bbc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 18 Mar 2026 12:40:33 +0100 Subject: [PATCH 03/13] Add test for first launch --- e2e/tests/connect/firstLaunch.spec.ts | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 e2e/tests/connect/firstLaunch.spec.ts diff --git a/e2e/tests/connect/firstLaunch.spec.ts b/e2e/tests/connect/firstLaunch.spec.ts new file mode 100644 index 0000000000000..e0c7f6f278d6d --- /dev/null +++ b/e2e/tests/connect/firstLaunch.spec.ts @@ -0,0 +1,50 @@ +/** + * Teleport + * Copyright (C) 2026 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { + expect, + initializeDataDir, + launchApp, + test, + withDefaultAppConfig, +} from '@gravitational/e2e/helpers/connect'; + +test('first launch shows usage data dialog', async () => { + await using temp = await fs.mkdtempDisposable( + path.join(os.tmpdir(), 'connect-e2e-first-launch-') + ); + // Set usageReporting.enabled to undefined so it's not stored in the config, which causes the + // usage data dialog to appear on launch. + await initializeDataDir( + temp.path, + withDefaultAppConfig({ 'usageReporting.enabled': undefined }) + ); + + await using app = await launchApp(temp.path); + const { page } = app; + + await expect(page.getByText('Anonymous usage data')).toBeVisible(); + await page.getByRole('button', { name: 'Decline', exact: true }).click(); + + // After dismissing the dialog, the app should show the default "Connect a Cluster" screen. + await expect(page.getByText('Connect a Cluster')).toBeVisible(); +}); From 1a5b00a09b8618a854896efd33f34d4a88c42de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 18 Mar 2026 17:44:12 +0100 Subject: [PATCH 04/13] Preserve app_state.json for the missing-tsh restart case --- e2e/tests/connect/stateRestoration.spec.ts | 31 +++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/e2e/tests/connect/stateRestoration.spec.ts b/e2e/tests/connect/stateRestoration.spec.ts index 36e54cb6bf55b..1ec4794e5dd56 100644 --- a/e2e/tests/connect/stateRestoration.spec.ts +++ b/e2e/tests/connect/stateRestoration.spec.ts @@ -75,11 +75,7 @@ test.describe('state restoration from disk', () => { } }); - test('missing state files do not crash the app', async () => { - const userDataDir = path.join(tempPath, 'userData'); - const appStatePath = path.join(userDataDir, 'app_state.json'); - const tshHomePath = path.join(tempPath, 'home', '.tsh'); - + test('missing app_state.json does not crash the app', async () => { // Login to create state files on disk. { await using app = await launchApp(tempPath); @@ -87,8 +83,10 @@ test.describe('state restoration from disk', () => { } // Remove app_state.json (keep tsh dir) – the app should not crash. + const appStatePath = path.join(tempPath, 'userData', 'app_state.json'); + await fs.rm(appStatePath); + { - await fs.rm(appStatePath); await using app = await launchApp(tempPath); // Without app_state.json, the app has no saved rootClusterUri, so no workspace is activated @@ -97,14 +95,29 @@ test.describe('state restoration from disk', () => { app.page.getByText('Log in to a cluster to use Teleport Connect.') ).toBeVisible(); } + }); - // Remove the tsh home directory – the app should not crash. + test('missing tsh home directory does not crash the app', async () => { + // Login to create state files on disk. { - await fs.rm(tshHomePath, { recursive: true }); await using app = await launchApp(tempPath); + await login(app.page); + } + + // Remove the tsh home directory (keep app_state.json) – the app should not crash. + const tshHomePath = path.join(tempPath, 'home', '.tsh'); + await fs.rm(tshHomePath, { recursive: true }); + + { + await using app = await launchApp(tempPath); + const { page } = app; // With no tsh dir, no cluster can be connected. - await expect(app.page.getByText('Connect a Cluster')).toBeVisible(); + await expect(page.getByText('Connect a Cluster')).toBeVisible(); + // app_state.json still references the old cluster, so setActiveWorkspace should show an error. + await expect( + page.getByText('Could not set cluster as active') + ).toBeVisible(); } }); From c0bcd68fb1d7f7f4c6f56cb4a0a78d3b1abb1a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 18 Mar 2026 17:54:50 +0100 Subject: [PATCH 05/13] Choose window bounds relative to the current display --- e2e/tests/connect/stateRestoration.spec.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/e2e/tests/connect/stateRestoration.spec.ts b/e2e/tests/connect/stateRestoration.spec.ts index 1ec4794e5dd56..667706459b1c2 100644 --- a/e2e/tests/connect/stateRestoration.spec.ts +++ b/e2e/tests/connect/stateRestoration.spec.ts @@ -190,12 +190,24 @@ test.describe('state restoration from disk', () => { test('window remembers size and position after restart', async () => { // Launch the app and resize the window. + let targetBounds: { x: number; y: number; width: number; height: number }; { await using app = await launchApp(tempPath); const { page } = app; await expect(page.getByText('Connect a Cluster')).toBeVisible(); - const targetBounds = { x: 100, y: 100, width: 900, height: 600 }; + // Pick bounds relative to the primary display so the test works on any screen size. + // WindowsManager.getWindowState restores saved bounds only when they fit entirely within + // a display, so the bounds must be within the display's work area. + targetBounds = await app.electronApp.evaluate(({ screen }) => { + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + return { + x: Math.floor(width * 0.1), + y: Math.floor(height * 0.1), + width: Math.floor(width * 0.6), + height: Math.floor(height * 0.6), + }; + }); await app.electronApp.evaluate(({ BrowserWindow }, bounds) => { const win = BrowserWindow.getAllWindows()[0]; win.setBounds(bounds); @@ -216,7 +228,7 @@ test.describe('state restoration from disk', () => { return win.getNormalBounds(); }); - expect(bounds).toEqual({ x: 100, y: 100, width: 900, height: 600 }); + expect(bounds).toEqual(targetBounds); } }); }); From ae98dca2265743a1c547ce6c0ffee441a054671a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 18 Mar 2026 18:38:03 +0100 Subject: [PATCH 06/13] Account for work area, read bounds after setting them --- e2e/tests/connect/stateRestoration.spec.ts | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/e2e/tests/connect/stateRestoration.spec.ts b/e2e/tests/connect/stateRestoration.spec.ts index 667706459b1c2..2b8e500ff6d04 100644 --- a/e2e/tests/connect/stateRestoration.spec.ts +++ b/e2e/tests/connect/stateRestoration.spec.ts @@ -196,22 +196,31 @@ test.describe('state restoration from disk', () => { const { page } = app; await expect(page.getByText('Connect a Cluster')).toBeVisible(); - // Pick bounds relative to the primary display so the test works on any screen size. + // Pick bounds relative to the primary display's work area so the test works on any screen + // size and on multi-monitor setups where the primary display may be offset from (0, 0). // WindowsManager.getWindowState restores saved bounds only when they fit entirely within - // a display, so the bounds must be within the display's work area. - targetBounds = await app.electronApp.evaluate(({ screen }) => { - const { width, height } = screen.getPrimaryDisplay().workAreaSize; + // a display. + const requestedBounds = await app.electronApp.evaluate(({ screen }) => { + const wa = screen.getPrimaryDisplay().workArea; return { - x: Math.floor(width * 0.1), - y: Math.floor(height * 0.1), - width: Math.floor(width * 0.6), - height: Math.floor(height * 0.6), + x: wa.x + Math.floor(wa.width * 0.1), + y: wa.y + Math.floor(wa.height * 0.1), + width: Math.floor(wa.width * 0.6), + height: Math.floor(wa.height * 0.6), }; }); await app.electronApp.evaluate(({ BrowserWindow }, bounds) => { const win = BrowserWindow.getAllWindows()[0]; win.setBounds(bounds); - }, targetBounds); + }, requestedBounds); + + // Read back the accepted bounds after the window manager has applied them. The WM may adjust + // the requested rectangle (e.g. on Wayland or tiling layouts), and the app persists + // getNormalBounds(), not the requested values. + targetBounds = await app.electronApp.evaluate(({ BrowserWindow }) => { + const win = BrowserWindow.getAllWindows()[0]; + return win.getNormalBounds(); + }); } // Relaunch – the window should restore to the same size & position. From e6802f346be53d20d2b2ff02bc5a8b7485bcddd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 19 Mar 2026 12:12:55 +0100 Subject: [PATCH 07/13] Assert absence of the restore modal after re-login --- e2e/tests/connect/stateRestoration.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/tests/connect/stateRestoration.spec.ts b/e2e/tests/connect/stateRestoration.spec.ts index 2b8e500ff6d04..32b54cf808bf3 100644 --- a/e2e/tests/connect/stateRestoration.spec.ts +++ b/e2e/tests/connect/stateRestoration.spec.ts @@ -154,6 +154,7 @@ test.describe('state restoration from disk', () => { await expect( page.locator('[role="tab"][data-doc-kind="doc.terminal_shell"]') ).toHaveCount(0); + await expect(page.getByText('Reopen previous session')).not.toBeVisible(); }); test('identical workspace shape does not trigger restore dialog', async () => { From 31eb10e411dffd907c24c7e1eedf41c186f27e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 19 Mar 2026 12:44:33 +0100 Subject: [PATCH 08/13] Assert that telemetry dialog is dismissed --- e2e/tests/connect/firstLaunch.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/e2e/tests/connect/firstLaunch.spec.ts b/e2e/tests/connect/firstLaunch.spec.ts index e0c7f6f278d6d..c4a4d7e0a4c77 100644 --- a/e2e/tests/connect/firstLaunch.spec.ts +++ b/e2e/tests/connect/firstLaunch.spec.ts @@ -42,9 +42,14 @@ test('first launch shows usage data dialog', async () => { await using app = await launchApp(temp.path); const { page } = app; - await expect(page.getByText('Anonymous usage data')).toBeVisible(); + const usageDataDialog = page.getByText('Anonymous usage data'); + await expect(usageDataDialog).toBeVisible(); await page.getByRole('button', { name: 'Decline', exact: true }).click(); + // Assert the dialog is dismissed – without this, the "Connect a Cluster" check below would pass + // even if clicking Decline failed, since that screen is already rendered under the modal. + await expect(usageDataDialog).not.toBeVisible(); + // After dismissing the dialog, the app should show the default "Connect a Cluster" screen. await expect(page.getByText('Connect a Cluster')).toBeVisible(); }); From 5d82a8da1ba379f98a13b0da66354c83d1685ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 19 Mar 2026 16:06:10 +0100 Subject: [PATCH 09/13] setActiveWorkspace: Test if rootClusterUri and documents-reopen are set in same tick --- .../workspacesService.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts index 3f80718dd3a7f..29b92405368bc 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts @@ -531,6 +531,53 @@ describe('setActiveWorkspace', () => { expect(workspacesService.getRootClusterUri()).toStrictEqual(clusterBar.uri); }); + + it('opens the documents-reopen dialog in the same tick as setting rootClusterUri', async () => { + const cluster = makeRootCluster(); + const testWorkspace: PersistedWorkspace = { + localClusterUri: cluster.uri, + documents: [ + { + kind: 'doc.terminal_shell', + uri: '/docs/terminal_shell_uri', + title: '/Users/alice/Documents', + }, + ], + location: '/docs/terminal_shell_uri', + }; + + const { workspacesService, modalsService } = getTestSetup({ + cluster, + persistedWorkspaces: { [cluster.uri]: testWorkspace }, + }); + + workspacesService.restorePersistedState(); + + // Queue a microtask before starting activation. If an await is ever introduced between setting + // rootClusterUri and opening the dialog, this microtask will execute before the dialog opens, + // causing the assertion below to fail. This invariant matters because e2e tests rely on React + // rendering rootClusterUri and the dialog in the same batch. + let microtaskRan = false; + queueMicrotask(() => { + microtaskRan = true; + }); + + jest + .spyOn(modalsService, 'openRegularDialog') + .mockImplementation(dialog => { + if (dialog.kind === 'documents-reopen') { + expect(microtaskRan).toBe(false); + expect(workspacesService.getRootClusterUri()).toBe(cluster.uri); + dialog.onDiscard(); + } + + return { + closeDialog: () => {}, + }; + }); + + await workspacesService.setActiveWorkspace(cluster.uri); + }); }); function getTestSetup(options: { From c051c8ad7b4c047e5710e0ea9f3799b0fb2f747c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 19 Mar 2026 16:26:38 +0100 Subject: [PATCH 10/13] Disable jest/no-conditional-expect --- .../src/ui/services/workspacesService/workspacesService.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts index 29b92405368bc..e7aeff10d7f9d 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts @@ -566,8 +566,10 @@ describe('setActiveWorkspace', () => { .spyOn(modalsService, 'openRegularDialog') .mockImplementation(dialog => { if (dialog.kind === 'documents-reopen') { + /* oxlint-disable jest/no-conditional-expect */ expect(microtaskRan).toBe(false); expect(workspacesService.getRootClusterUri()).toBe(cluster.uri); + /* oxlint-enable jest/no-conditional-expect */ dialog.onDiscard(); } From 75797519b95e86070bcaff16458c6d0bebe520f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 19 Mar 2026 16:36:38 +0100 Subject: [PATCH 11/13] Assert the reopen dialog closes after restoring tabs --- e2e/tests/connect/stateRestoration.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/tests/connect/stateRestoration.spec.ts b/e2e/tests/connect/stateRestoration.spec.ts index 32b54cf808bf3..9daa61db7da9b 100644 --- a/e2e/tests/connect/stateRestoration.spec.ts +++ b/e2e/tests/connect/stateRestoration.spec.ts @@ -64,6 +64,7 @@ test.describe('state restoration from disk', () => { const { page } = app; await expect(page.getByText('Reopen previous session')).toBeVisible(); await page.getByRole('button', { name: 'Reopen' }).click(); + await expect(page.getByText('Reopen previous session')).not.toBeVisible(); // Both tabs should be restored. await expect( From 93041be93944bc1f1681700454102ceb8c797a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 19 Mar 2026 17:23:20 +0100 Subject: [PATCH 12/13] Verify that documents-reopen was called --- .../workspacesService/workspacesService.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts index e7aeff10d7f9d..adde6e1ac4e35 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/workspacesService.test.ts @@ -565,11 +565,10 @@ describe('setActiveWorkspace', () => { jest .spyOn(modalsService, 'openRegularDialog') .mockImplementation(dialog => { + expect(dialog.kind).toEqual('documents-reopen'); + expect(microtaskRan).toBe(false); + expect(workspacesService.getRootClusterUri()).toBe(cluster.uri); if (dialog.kind === 'documents-reopen') { - /* oxlint-disable jest/no-conditional-expect */ - expect(microtaskRan).toBe(false); - expect(workspacesService.getRootClusterUri()).toBe(cluster.uri); - /* oxlint-enable jest/no-conditional-expect */ dialog.onDiscard(); } @@ -579,6 +578,10 @@ describe('setActiveWorkspace', () => { }); await workspacesService.setActiveWorkspace(cluster.uri); + expect(modalsService.openRegularDialog).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'documents-reopen' }), + expect.any(AbortSignal) + ); }); }); From dfa51173915389400bd727234b52fe0960d31573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 19 Mar 2026 17:26:01 +0100 Subject: [PATCH 13/13] Verify setBounds works --- e2e/tests/connect/stateRestoration.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/e2e/tests/connect/stateRestoration.spec.ts b/e2e/tests/connect/stateRestoration.spec.ts index 9daa61db7da9b..98aa21169ce98 100644 --- a/e2e/tests/connect/stateRestoration.spec.ts +++ b/e2e/tests/connect/stateRestoration.spec.ts @@ -211,6 +211,15 @@ test.describe('state restoration from disk', () => { height: Math.floor(wa.height * 0.6), }; }); + + // Capture the initial bounds before resizing so we can verify setBounds() had an effect. + const initialBounds = await app.electronApp.evaluate( + ({ BrowserWindow }) => { + const win = BrowserWindow.getAllWindows()[0]; + return win.getNormalBounds(); + } + ); + await app.electronApp.evaluate(({ BrowserWindow }, bounds) => { const win = BrowserWindow.getAllWindows()[0]; win.setBounds(bounds); @@ -223,6 +232,10 @@ test.describe('state restoration from disk', () => { const win = BrowserWindow.getAllWindows()[0]; return win.getNormalBounds(); }); + + // Verify the window actually moved. On environments where setBounds() is a no-op (e.g. + // Wayland, tiling WMs), the relaunch assertion would pass trivially without this guard. + expect(targetBounds).not.toEqual(initialBounds); } // Relaunch – the window should restore to the same size & position.