mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
remove activeEnvironmentUid and migration (#7545)
* remove activeEnvironmentUid and migration * fix: no environment handling * fix: standardize workspace path handling
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Test Collection",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
name: Alpha
|
||||
variables:
|
||||
- name: mode
|
||||
value: alpha
|
||||
@@ -0,0 +1,4 @@
|
||||
name: Beta
|
||||
variables:
|
||||
- name: mode
|
||||
value: beta
|
||||
@@ -0,0 +1,12 @@
|
||||
opencollection: 1.0.0
|
||||
info:
|
||||
name: "My Workspace"
|
||||
type: workspace
|
||||
|
||||
collections:
|
||||
- name: "Test Collection"
|
||||
path: "collections/test-collection"
|
||||
|
||||
specs:
|
||||
|
||||
docs: ''
|
||||
@@ -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<string> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"preferences": {
|
||||
"onboarding": {
|
||||
"hasLaunchedBefore": true,
|
||||
"hasSeenWelcomeModal": true
|
||||
},
|
||||
"general": {
|
||||
"defaultWorkspacePath": "{{workspacePath}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"preferences": {
|
||||
"onboarding": {
|
||||
"hasLaunchedBefore": true,
|
||||
"hasSeenWelcomeModal": true
|
||||
},
|
||||
"general": {
|
||||
"defaultLocation": "{{wsLocation}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user