diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js index 0028353d0..e3dcc9c59 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js @@ -737,28 +737,6 @@ export const deleteWorkspaceEnvironment = (workspaceUid, environmentUid) => { }; }; -export const selectWorkspaceEnvironment = (workspaceUid, environmentUid) => { - return async (dispatch, getState) => { - try { - const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid); - if (!workspace) { - throw new Error('Workspace not found'); - } - - await ipcRenderer.invoke('renderer:select-workspace-environment', workspace.pathname, environmentUid); - - dispatch(updateWorkspace({ - uid: workspaceUid, - activeEnvironmentUid: environmentUid - })); - - return true; - } catch (error) { - throw error; - } - }; -}; - export const importWorkspaceEnvironment = (workspaceUid, environmentData) => { return async (dispatch, getState) => { try { diff --git a/packages/bruno-electron/src/ipc/global-environments.js b/packages/bruno-electron/src/ipc/global-environments.js index d2d8ec018..a616b113c 100644 --- a/packages/bruno-electron/src/ipc/global-environments.js +++ b/packages/bruno-electron/src/ipc/global-environments.js @@ -5,6 +5,49 @@ const { ipcMain } = require('electron'); const { utils: { jsonToDotenv } } = require('@usebruno/common'); const { globalEnvironmentsStore } = require('../store/global-environments'); const { generateUniqueName, sanitizeName, writeFile, isValidDotEnvFilename } = require('../utils/filesystem'); +const { readWorkspaceConfig, writeWorkspaceConfig } = require('../utils/workspace-config'); + +/** + * Migrates activeEnvironmentUid from workspace.yml to the electron store (per-workspace). + * This handles users upgrading from versions that stored the active global env in workspace.yml. + * + * Fallback chain: + * 1. Per-workspace electron store (already migrated) - use it + * 2. workspace.yml activeEnvironmentUid - migrate to electron store, remove from file + * 3. Legacy electron store activeGlobalEnvironmentUid - migrate to per-workspace store + * 4. null (new workspace) + */ +const migrateActiveGlobalEnvironmentUid = async (workspacePath) => { + // Already in per-workspace store (null means explicitly "No Environment", undefined means not set) + const perWorkspaceUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUidForWorkspace(workspacePath); + if (perWorkspaceUid !== undefined) { + return perWorkspaceUid; + } + + // Try workspace.yml + try { + const config = readWorkspaceConfig(workspacePath); + if (config.activeEnvironmentUid) { + const uid = config.activeEnvironmentUid; + // Migrate to electron store + globalEnvironmentsStore.setActiveGlobalEnvironmentUidForWorkspace(workspacePath, uid); + // Rewrite workspace.yml without activeEnvironmentUid (generateYamlContent drops unknown fields) + await writeWorkspaceConfig(workspacePath, config); + return uid; + } + } catch (error) { + // workspace.yml may not exist or be unreadable, continue to next fallback + } + + // Fallback to legacy single active uid + const legacyUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUid(); + if (legacyUid) { + globalEnvironmentsStore.setActiveGlobalEnvironmentUidForWorkspace(workspacePath, legacyUid); + return legacyUid; + } + + return null; +}; const registerGlobalEnvironmentsIpc = (mainWindow, workspaceEnvironmentsManager) => { ipcMain.handle('renderer:create-global-environment', async (event, { uid, name, variables, color, workspaceUid, workspacePath }) => { @@ -64,23 +107,28 @@ const registerGlobalEnvironmentsIpc = (mainWindow, workspaceEnvironmentsManager) ipcMain.handle('renderer:delete-global-environment', async (event, { environmentUid, workspaceUid, workspacePath }) => { try { if (workspacePath && workspaceEnvironmentsManager) { - return await workspaceEnvironmentsManager.deleteGlobalEnvironmentByPath(workspacePath, { environmentUid }); + await workspaceEnvironmentsManager.deleteGlobalEnvironmentByPath(workspacePath, { environmentUid }); + // Clear active environment for this workspace if the deleted one was active + const activeUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUidForWorkspace(workspacePath); + if (activeUid === environmentUid) { + globalEnvironmentsStore.setActiveGlobalEnvironmentUidForWorkspace(workspacePath, null); + } + } else { + globalEnvironmentsStore.deleteGlobalEnvironment({ environmentUid }); } - - globalEnvironmentsStore.deleteGlobalEnvironment({ environmentUid }); } catch (error) { console.error('Error in renderer:delete-global-environment:', error); return Promise.reject(error); } }); - ipcMain.handle('renderer:select-global-environment', async (event, { environmentUid, workspaceUid, workspacePath }) => { + ipcMain.handle('renderer:select-global-environment', async (event, { environmentUid, workspacePath }) => { try { - if (workspacePath && workspaceEnvironmentsManager) { - return await workspaceEnvironmentsManager.selectGlobalEnvironmentByPath(workspacePath, { environmentUid }); + if (workspacePath) { + globalEnvironmentsStore.setActiveGlobalEnvironmentUidForWorkspace(workspacePath, environmentUid || null); + } else { + globalEnvironmentsStore.setActiveGlobalEnvironmentUid(environmentUid || null); } - - globalEnvironmentsStore.selectGlobalEnvironment({ environmentUid }); } catch (error) { console.error('Error in renderer:select-global-environment:', error); return Promise.reject(error); @@ -89,13 +137,22 @@ const registerGlobalEnvironmentsIpc = (mainWindow, workspaceEnvironmentsManager) ipcMain.handle('renderer:get-global-environments', async (event, { workspaceUid, workspacePath }) => { try { + let globalEnvironments = []; + if (workspacePath && workspaceEnvironmentsManager) { - return await workspaceEnvironmentsManager.getGlobalEnvironmentsByPath(workspacePath); + const result = await workspaceEnvironmentsManager.getGlobalEnvironmentsByPath(workspacePath); + globalEnvironments = result?.globalEnvironments || []; + } else { + globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments() || []; } + const activeGlobalEnvironmentUid = workspacePath + ? await migrateActiveGlobalEnvironmentUid(workspacePath) + : globalEnvironmentsStore.getActiveGlobalEnvironmentUid(); + return { - globalEnvironments: globalEnvironmentsStore.getGlobalEnvironments() || [], - activeGlobalEnvironmentUid: globalEnvironmentsStore.getActiveGlobalEnvironmentUid() + globalEnvironments, + activeGlobalEnvironmentUid }; } catch (error) { console.error('Error in renderer:get-global-environments:', error); diff --git a/packages/bruno-electron/src/ipc/workspace.js b/packages/bruno-electron/src/ipc/workspace.js index a896a2a09..342d93ce1 100644 --- a/packages/bruno-electron/src/ipc/workspace.js +++ b/packages/bruno-electron/src/ipc/workspace.js @@ -10,6 +10,7 @@ const yaml = require('js-yaml'); const LastOpenedWorkspaces = require('../store/last-opened-workspaces'); const { defaultWorkspaceManager } = require('../store/default-workspace'); const { globalEnvironmentsManager } = require('../store/workspace-environments'); +const { globalEnvironmentsStore } = require('../store/global-environments'); const { createWorkspaceConfig, @@ -280,6 +281,7 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => { ipcMain.handle('renderer:close-workspace', async (event, workspacePath) => { try { lastOpenedWorkspaces.remove(workspacePath); + globalEnvironmentsStore.removeActiveGlobalEnvironmentUidForWorkspace(workspacePath); if (workspaceWatcher) { workspaceWatcher.removeWatcher(workspacePath); @@ -468,14 +470,6 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => { } }); - ipcMain.handle('renderer:select-workspace-environment', async (event, workspacePath, environmentUid) => { - try { - return await globalEnvironmentsManager.selectGlobalEnvironment(workspacePath, { environmentUid }); - } catch (error) { - throw error; - } - }); - ipcMain.handle('renderer:import-workspace-environment', async (event, workspacePath, environmentData) => { try { return await globalEnvironmentsManager.createGlobalEnvironment(workspacePath, { diff --git a/packages/bruno-electron/src/store/default-workspace.js b/packages/bruno-electron/src/store/default-workspace.js index e3c41668a..203a10cc9 100644 --- a/packages/bruno-electron/src/store/default-workspace.js +++ b/packages/bruno-electron/src/store/default-workspace.js @@ -66,7 +66,7 @@ class DefaultWorkspaceManager { * Recovers collections and environments from an existing workspace directory */ recoverDataFromWorkspace(workspacePath) { - const recovered = { collections: [], environments: [], activeEnvironmentUid: null }; + const recovered = { collections: [], environments: [] }; try { // Try to read workspace config for collections @@ -78,9 +78,6 @@ class DefaultWorkspaceManager { return isValidCollectionDirectory(collectionPath); }); } - if (config.activeEnvironmentUid) { - recovered.activeEnvironmentUid = config.activeEnvironmentUid; - } } catch (error) { console.error('Failed to read workspace config during recovery:', error); } @@ -273,9 +270,6 @@ class DefaultWorkspaceManager { console.error('Failed to copy environment:', env.name, error); } } - if (recoveredData.activeEnvironmentUid) { - workspaceConfig.activeEnvironmentUid = recoveredData.activeEnvironmentUid; - } } // Apply recovered collections first (lower priority) @@ -374,8 +368,12 @@ class DefaultWorkspaceManager { const content = stringifyEnvironment(environment, { format: 'yml' }); await writeFile(envFilePath, content); - if (env.uid === activeGlobalEnvironmentUid && !workspaceConfig.activeEnvironmentUid) { - workspaceConfig.activeEnvironmentUid = generateUidBasedOnHash(envFilePath); + // Map the legacy active env uid to the new file-based uid in the per-workspace store + if (env.uid === activeGlobalEnvironmentUid) { + globalEnvironmentsStore.setActiveGlobalEnvironmentUidForWorkspace( + workspacePath, + generateUidBasedOnHash(envFilePath) + ); } } } diff --git a/packages/bruno-electron/src/store/global-environments.js b/packages/bruno-electron/src/store/global-environments.js index e5c7b484e..8b4447d8a 100644 --- a/packages/bruno-electron/src/store/global-environments.js +++ b/packages/bruno-electron/src/store/global-environments.js @@ -2,6 +2,7 @@ const _ = require('lodash'); const Store = require('electron-store'); const { encryptStringSafe, decryptStringSafe } = require('../utils/encryption'); const { environmentSchema } = require('@usebruno/schema'); +const { posixifyPath } = require('../utils/filesystem'); class GlobalEnvironmentsStore { constructor() { @@ -86,6 +87,36 @@ class GlobalEnvironmentsStore { return this.store.get('activeGlobalEnvironmentUid', null); } + setActiveGlobalEnvironmentUid(uid) { + return this.store.set('activeGlobalEnvironmentUid', uid); + } + + getActiveGlobalEnvironmentUidForWorkspace(workspacePath) { + if (!workspacePath) return undefined; + const key = posixifyPath(workspacePath); + const mapping = this.store.get('activeGlobalEnvironmentUidByWorkspace', {}); + if (key in mapping) { + return mapping[key]; + } + return undefined; + } + + setActiveGlobalEnvironmentUidForWorkspace(workspacePath, uid) { + if (!workspacePath) return; + const key = posixifyPath(workspacePath); + const mapping = this.store.get('activeGlobalEnvironmentUidByWorkspace', {}); + mapping[key] = uid || null; + this.store.set('activeGlobalEnvironmentUidByWorkspace', mapping); + } + + removeActiveGlobalEnvironmentUidForWorkspace(workspacePath) { + if (!workspacePath) return; + const key = posixifyPath(workspacePath); + const mapping = this.store.get('activeGlobalEnvironmentUidByWorkspace', {}); + delete mapping[key]; + this.store.set('activeGlobalEnvironmentUidByWorkspace', mapping); + } + setGlobalEnvironments(globalEnvironments) { globalEnvironments = this.filterValidEnvironments(globalEnvironments); @@ -93,10 +124,6 @@ class GlobalEnvironmentsStore { return this.store.set('environments', globalEnvironments); } - setActiveGlobalEnvironmentUid(uid) { - return this.store.set('activeGlobalEnvironmentUid', uid); - } - addGlobalEnvironment({ uid, name, variables = [], color }) { let globalEnvironments = this.getGlobalEnvironments(); const existingEnvironment = globalEnvironments.find((env) => env?.name == name); diff --git a/packages/bruno-electron/src/store/workspace-environments.js b/packages/bruno-electron/src/store/workspace-environments.js index f10c3d2b2..688678ab4 100644 --- a/packages/bruno-electron/src/store/workspace-environments.js +++ b/packages/bruno-electron/src/store/workspace-environments.js @@ -1,18 +1,11 @@ const fs = require('fs'); const path = require('path'); const _ = require('lodash'); -const yaml = require('js-yaml'); const { parseEnvironment, stringifyEnvironment } = require('@usebruno/filestore'); const { writeFile, createDirectory } = require('../utils/filesystem'); const { generateUidBasedOnHash, uuid } = require('../utils/common'); const { decryptStringSafe } = require('../utils/encryption'); const EnvironmentSecretsStore = require('./env-secrets'); -const { - readWorkspaceConfig, - generateYamlContent, - writeWorkspaceFileAtomic -} = require('../utils/workspace-config'); -const { withLock, getWorkspaceLockKey } = require('../utils/workspace-lock'); const environmentSecretsStore = new EnvironmentSecretsStore(); @@ -98,8 +91,7 @@ class GlobalEnvironmentsManager { if (!fs.existsSync(environmentsDir)) { return { - globalEnvironments: [], - activeGlobalEnvironmentUid: null + globalEnvironments: [] }; } @@ -119,58 +111,14 @@ class GlobalEnvironmentsManager { } } - const activeGlobalEnvironmentUid = await this.getActiveGlobalEnvironmentUid(workspacePath); - return { - globalEnvironments: environments, - activeGlobalEnvironmentUid + globalEnvironments: environments }; } catch (error) { throw error; } } - async getActiveGlobalEnvironmentUid(workspacePath) { - try { - if (!workspacePath) { - return null; - } - - const workspaceFilePath = path.join(workspacePath, 'workspace.yml'); - - if (!fs.existsSync(workspaceFilePath)) { - return null; - } - - const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8'); - const workspaceConfig = yaml.load(yamlContent); - - return workspaceConfig.activeEnvironmentUid || null; - } catch (error) { - return null; - } - } - - async setActiveGlobalEnvironmentUid(workspacePath, environmentUid) { - if (!workspacePath) { - throw new Error('Workspace path is required'); - } - - const workspaceFilePath = path.join(workspacePath, 'workspace.yml'); - - if (!fs.existsSync(workspaceFilePath)) { - throw new Error('Invalid workspace: workspace.yml not found'); - } - - return withLock(getWorkspaceLockKey(workspacePath), async () => { - const workspaceConfig = readWorkspaceConfig(workspacePath); - workspaceConfig.activeEnvironmentUid = environmentUid; - const yamlOutput = generateYamlContent(workspaceConfig); - await writeWorkspaceFileAtomic(workspacePath, yamlOutput); - return true; - }); - } - async createGlobalEnvironment(workspacePath, { uid, name, variables, color }) { try { if (!workspacePath) { @@ -303,24 +251,6 @@ class GlobalEnvironmentsManager { fs.unlinkSync(envFile.filePath); - const activeGlobalEnvironmentUid = await this.getActiveGlobalEnvironmentUid(workspacePath); - if (activeGlobalEnvironmentUid === environmentUid) { - await this.setActiveGlobalEnvironmentUid(workspacePath, null); - } - - return true; - } catch (error) { - throw error; - } - } - - async selectGlobalEnvironment(workspacePath, { environmentUid }) { - try { - if (!workspacePath) { - throw new Error('Workspace path is required'); - } - - await this.setActiveGlobalEnvironmentUid(workspacePath, environmentUid); return true; } catch (error) { throw error; @@ -371,10 +301,6 @@ class GlobalEnvironmentsManager { return this.deleteGlobalEnvironment(workspacePath, params); } - async selectGlobalEnvironmentByPath(workspacePath, params) { - return this.selectGlobalEnvironment(workspacePath, params); - } - async updateGlobalEnvironmentColorByPath(workspacePath, { environmentUid, color }) { return this.updateGlobalEnvironmentColor(workspacePath, environmentUid, color); } diff --git a/packages/bruno-electron/src/utils/workspace-config.js b/packages/bruno-electron/src/utils/workspace-config.js index 7e0c6d900..b02c49777 100644 --- a/packages/bruno-electron/src/utils/workspace-config.js +++ b/packages/bruno-electron/src/utils/workspace-config.js @@ -272,11 +272,6 @@ const generateYamlContent = (config) => { yamlLines.push('docs: \'\''); } - if (config.activeEnvironmentUid && typeof config.activeEnvironmentUid === 'string') { - yamlLines.push(''); - yamlLines.push(`activeEnvironmentUid: ${config.activeEnvironmentUid}`); - } - yamlLines.push(''); return yamlLines.join('\n'); diff --git a/tests/environments/global-env-migration-from-file/fixtures/workspace/collections/test-collection/bruno.json b/tests/environments/global-env-migration-from-file/fixtures/workspace/collections/test-collection/bruno.json new file mode 100644 index 000000000..948028cbd --- /dev/null +++ b/tests/environments/global-env-migration-from-file/fixtures/workspace/collections/test-collection/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "Test Collection", + "type": "collection" +} diff --git a/tests/environments/global-env-migration-from-file/fixtures/workspace/environments/Alpha.yml b/tests/environments/global-env-migration-from-file/fixtures/workspace/environments/Alpha.yml new file mode 100644 index 000000000..a4b318bf3 --- /dev/null +++ b/tests/environments/global-env-migration-from-file/fixtures/workspace/environments/Alpha.yml @@ -0,0 +1,4 @@ +name: Alpha +variables: + - name: mode + value: alpha diff --git a/tests/environments/global-env-migration-from-file/fixtures/workspace/environments/Beta.yml b/tests/environments/global-env-migration-from-file/fixtures/workspace/environments/Beta.yml new file mode 100644 index 000000000..727679507 --- /dev/null +++ b/tests/environments/global-env-migration-from-file/fixtures/workspace/environments/Beta.yml @@ -0,0 +1,4 @@ +name: Beta +variables: + - name: mode + value: beta diff --git a/tests/environments/global-env-migration-from-file/fixtures/workspace/workspace.yml b/tests/environments/global-env-migration-from-file/fixtures/workspace/workspace.yml new file mode 100644 index 000000000..4a584a119 --- /dev/null +++ b/tests/environments/global-env-migration-from-file/fixtures/workspace/workspace.yml @@ -0,0 +1,12 @@ +opencollection: 1.0.0 +info: + name: "My Workspace" + type: workspace + +collections: + - name: "Test Collection" + path: "collections/test-collection" + +specs: + +docs: '' diff --git a/tests/environments/global-env-migration-from-file/global-env-migration-from-file.spec.ts b/tests/environments/global-env-migration-from-file/global-env-migration-from-file.spec.ts new file mode 100644 index 000000000..040caca39 --- /dev/null +++ b/tests/environments/global-env-migration-from-file/global-env-migration-from-file.spec.ts @@ -0,0 +1,92 @@ +import path from 'path'; +import fs from 'fs'; +import { test, expect, closeElectronApp } from '../../../playwright'; +import { openCollection } from '../../utils/page'; + +const initUserDataPath = path.join(__dirname, 'init-user-data'); +const workspaceFixturePath = path.join(__dirname, 'fixtures', 'workspace'); + +/** + * Replicate the uid generation from bruno-electron/src/utils/common.js + * so we can compute environment uids at test time. + */ +function simpleHash(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash &= hash; + } + return new Uint32Array([hash])[0].toString(36); +} + +function generateUidBasedOnHash(str: string): string { + const hash = simpleHash(str); + return `${hash}`.padEnd(21, '0'); +} + +/** + * Copy the workspace fixture to a temp location and return the path. + */ +async function copyWorkspaceFixture(destDir: string): Promise { + const workspacePath = path.join(destDir, 'workspace'); + await fs.promises.cp(workspaceFixturePath, workspacePath, { recursive: true }); + return workspacePath; +} + +test.describe('Global Environment Migration from workspace.yml', () => { + test('should migrate activeEnvironmentUid from workspace.yml to electron store and remove from file', async ({ + launchElectronApp, + createTmpDir + }) => { + const userDataPath = await createTmpDir('env-migrate-from-file'); + const fixtureDir = await createTmpDir('ws-fixture'); + + // Copy workspace fixture to temp location + const workspacePath = await copyWorkspaceFixture(fixtureDir); + + // Compute uid for the Alpha environment file at its actual path + const alphaFilePath = path.join(workspacePath, 'environments', 'Alpha.yml'); + const alphaUid = generateUidBasedOnHash(alphaFilePath); + + // Inject activeEnvironmentUid into workspace.yml (simulating pre-migration state) + const workspaceYmlPath = path.join(workspacePath, 'workspace.yml'); + let workspaceYml = fs.readFileSync(workspaceYmlPath, 'utf8'); + workspaceYml = workspaceYml.replace( + 'collections:', + `activeEnvironmentUid: "${alphaUid}"\n\ncollections:` + ); + fs.writeFileSync(workspaceYmlPath, workspaceYml); + + // Launch with init-user-data pointing to the workspace + const app1 = await launchElectronApp({ + initUserDataPath, + userDataPath, + templateVars: { workspacePath } + }); + const page1 = await app1.firstWindow(); + await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + + // Open the collection so the env selector toolbar is visible + await openCollection(page1, 'Test Collection'); + + // Verify "Alpha" environment is selected (migrated from workspace.yml) + await expect(page1.locator('.current-environment')).toContainText('Alpha'); + + // Verify workspace.yml no longer contains activeEnvironmentUid + const updatedYml = fs.readFileSync(workspaceYmlPath, 'utf8'); + expect(updatedYml).not.toContain('activeEnvironmentUid'); + + await closeElectronApp(app1); + + // Restart — should still have Alpha selected (now from electron store) + const app2 = await launchElectronApp({ userDataPath }); + const page2 = await app2.firstWindow(); + await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + + await openCollection(page2, 'Test Collection'); + await expect(page2.locator('.current-environment')).toContainText('Alpha'); + + await closeElectronApp(app2); + }); +}); diff --git a/tests/environments/global-env-migration-from-file/init-user-data/preferences.json b/tests/environments/global-env-migration-from-file/init-user-data/preferences.json new file mode 100644 index 000000000..f8eb21742 --- /dev/null +++ b/tests/environments/global-env-migration-from-file/init-user-data/preferences.json @@ -0,0 +1,11 @@ +{ + "preferences": { + "onboarding": { + "hasLaunchedBefore": true, + "hasSeenWelcomeModal": true + }, + "general": { + "defaultWorkspacePath": "{{workspacePath}}" + } + } +} diff --git a/tests/environments/global-env-workspace-persistence/global-env-workspace-persistence.spec.ts b/tests/environments/global-env-workspace-persistence/global-env-workspace-persistence.spec.ts new file mode 100644 index 000000000..8bc00bcf8 --- /dev/null +++ b/tests/environments/global-env-workspace-persistence/global-env-workspace-persistence.spec.ts @@ -0,0 +1,114 @@ +import path from 'path'; +import { test, expect, closeElectronApp } from '../../../playwright'; +import { + createWorkspace, + switchWorkspace, + createCollection, + createEnvironment, + openCollection +} from '../../utils/page'; + +const initUserDataPath = path.join(__dirname, 'init-user-data'); + +test.describe('Global Environment Per-Workspace Persistence', () => { + test('should persist selected global environment across app restart', async ({ launchElectronApp, createTmpDir }) => { + const userDataPath = await createTmpDir('global-env-persist'); + const wsLocation = await createTmpDir('ws-location'); + const collectionDir = await createTmpDir('collection-persist'); + + // First launch + const app1 = await launchElectronApp({ + initUserDataPath, + userDataPath, + templateVars: { wsLocation } + }); + const page1 = await app1.firstWindow(); + await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + + // Create a collection so the environment selector is visible + await createCollection(page1, 'Test Collection', collectionDir); + + // Create a global environment (createEnvironment also selects it) + await createEnvironment(page1, 'Persist Test Env', 'global'); + await expect(page1.locator('.current-environment')).toContainText('Persist Test Env'); + + await closeElectronApp(app1); + + // Second launch - same userDataPath to preserve electron store + const app2 = await launchElectronApp({ userDataPath }); + const page2 = await app2.firstWindow(); + await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + + // Open the collection so the env selector is visible + await openCollection(page2, 'Test Collection'); + + // Verify the global environment is still selected after restart + await expect(page2.locator('.current-environment')).toContainText('Persist Test Env'); + + await closeElectronApp(app2); + }); + + test('should maintain independent global env selections per workspace', async ({ launchElectronApp, createTmpDir }) => { + const userDataPath = await createTmpDir('global-env-per-ws'); + const wsLocation = await createTmpDir('ws-location-multi'); + const collectionDir1 = await createTmpDir('collection-ws1'); + const collectionDir2 = await createTmpDir('collection-ws2'); + + const app = await launchElectronApp({ + initUserDataPath, + userDataPath, + templateVars: { wsLocation } + }); + const page = await app.firstWindow(); + await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + + // On the default workspace, create a collection and a global env + await createCollection(page, 'WS1 Collection', collectionDir1); + await createEnvironment(page, 'Env Alpha', 'global'); + await expect(page.locator('.current-environment')).toContainText('Env Alpha'); + + // Create a second workspace + await createWorkspace(page, 'Second Workspace'); + + // On the second workspace, create a collection and a different global env + await createCollection(page, 'WS2 Collection', collectionDir2); + await createEnvironment(page, 'Env Beta', 'global'); + await expect(page.locator('.current-environment')).toContainText('Env Beta'); + + // Switch back to first workspace - "Env Alpha" should still be selected + await switchWorkspace(page, 'My Workspace'); + await openCollection(page, 'WS1 Collection'); + await expect(page.locator('.current-environment')).toContainText('Env Alpha'); + + // Switch to second workspace - "Env Beta" should still be selected + await switchWorkspace(page, 'Second Workspace'); + await openCollection(page, 'WS2 Collection'); + await expect(page.locator('.current-environment')).toContainText('Env Beta'); + + await closeElectronApp(app); + + // Restart app and verify persistence across restart + const app2 = await launchElectronApp({ userDataPath }); + const page2 = await app2.firstWindow(); + await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + + // App opens to last active workspace - verify its env is still selected + const currentWorkspace = await page2.getByTestId('workspace-name').textContent(); + + if (currentWorkspace === 'Second Workspace') { + await openCollection(page2, 'WS2 Collection'); + await expect(page2.locator('.current-environment')).toContainText('Env Beta'); + await switchWorkspace(page2, 'My Workspace'); + await openCollection(page2, 'WS1 Collection'); + await expect(page2.locator('.current-environment')).toContainText('Env Alpha'); + } else { + await openCollection(page2, 'WS1 Collection'); + await expect(page2.locator('.current-environment')).toContainText('Env Alpha'); + await switchWorkspace(page2, 'Second Workspace'); + await openCollection(page2, 'WS2 Collection'); + await expect(page2.locator('.current-environment')).toContainText('Env Beta'); + } + + await closeElectronApp(app2); + }); +}); diff --git a/tests/environments/global-env-workspace-persistence/init-user-data/preferences.json b/tests/environments/global-env-workspace-persistence/init-user-data/preferences.json new file mode 100644 index 000000000..717f19028 --- /dev/null +++ b/tests/environments/global-env-workspace-persistence/init-user-data/preferences.json @@ -0,0 +1,11 @@ +{ + "preferences": { + "onboarding": { + "hasLaunchedBefore": true, + "hasSeenWelcomeModal": true + }, + "general": { + "defaultLocation": "{{wsLocation}}" + } + } +} diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index c19d9b2e8..edbc8bac0 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -1047,6 +1047,41 @@ const closeAllTabs = async (page: Page) => { }); }; +/** + * Create a new workspace via the title bar dropdown inline rename flow + * @param page - The page object + * @param workspaceName - The name of the workspace to create + * @returns void + */ +const createWorkspace = async (page: Page, workspaceName: string) => { + await test.step(`Create workspace "${workspaceName}"`, async () => { + await page.locator('.workspace-name-container').click(); + await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click(); + + const renameInput = page.locator('.workspace-name-input'); + await expect(renameInput).toBeVisible({ timeout: 5000 }); + await renameInput.fill(workspaceName); + await renameInput.press('Enter'); + + await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId('workspace-name')).toHaveText(workspaceName, { timeout: 5000 }); + }); +}; + +/** + * Switch to an existing workspace via the title bar dropdown + * @param page - The page object + * @param workspaceName - The name of the workspace to switch to + * @returns void + */ +const switchWorkspace = async (page: Page, workspaceName: string) => { + await test.step(`Switch to workspace "${workspaceName}"`, async () => { + await page.locator('.workspace-name-container').click(); + await page.locator('.workspace-item, .dropdown-item').filter({ hasText: workspaceName }).click(); + await expect(page.getByTestId('workspace-name')).toHaveText(workspaceName, { timeout: 5000 }); + }); +}; + export { closeAllCollections, openCollection, @@ -1082,7 +1117,9 @@ export { editAssertion, deleteAssertion, saveRequest, - closeAllTabs + closeAllTabs, + createWorkspace, + switchWorkspace }; export type { SandboxMode, EnvironmentType, EnvironmentVariable, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions, CreateTransientRequestOptions, AssertionInput };