fix: default workspace error checking (#6379)

* fix: default workspace error checking

* add: tests

* fixes

* fix

* fixes

* fixes

* fix

* fixes

* fix

* chore: close app context in tests

---------

Co-authored-by: Bijin A B <bijin@usebruno.com>
This commit is contained in:
naman-bruno
2025-12-15 15:10:53 +05:30
committed by GitHub
parent 1b9ea478da
commit 8b0f41e3cb
8 changed files with 562 additions and 93 deletions

View File

@@ -1,7 +1,7 @@
const { dialog, ipcMain } = require('electron');
const { normalizeAndResolvePath } = require('../utils/filesystem');
const { generateUidBasedOnHash } = require('../utils/common');
const { generateYamlContent } = require('../utils/workspace-config');
const { generateYamlContent, getWorkspaceUid } = require('../utils/workspace-config');
const normalizeWorkspaceConfig = (config) => {
return {
@@ -75,8 +75,12 @@ const openApiSpec = async (win, watcher, apiSpecPath, options = {}) => {
fs.writeFileSync(workspaceFilePath, updatedYamlContent);
const normalizedConfig = normalizeWorkspaceConfig(workspaceConfig);
const workspaceUid = generateUidBasedOnHash(options.workspacePath);
win.webContents.send('main:workspace-config-updated', options.workspacePath, workspaceUid, normalizedConfig);
const workspaceUid = getWorkspaceUid(options.workspacePath);
const isDefault = workspaceUid === 'default';
win.webContents.send('main:workspace-config-updated', options.workspacePath, workspaceUid, {
...normalizedConfig,
type: isDefault ? 'default' : normalizedConfig.type
});
}
}
}

View File

@@ -4,6 +4,7 @@ const path = require('path');
const chokidar = require('chokidar');
const yaml = require('js-yaml');
const { generateUidBasedOnHash, uuid } = require('../utils/common');
const { getWorkspaceUid } = require('../utils/workspace-config');
const { parseEnvironment } = require('@usebruno/filestore');
const EnvironmentSecretsStore = require('../store/env-secrets');
const { decryptStringSafe } = require('../utils/encryption');
@@ -42,9 +43,13 @@ const handleWorkspaceFileChange = (win, workspacePath) => {
return;
}
const workspaceUid = generateUidBasedOnHash(workspacePath);
const workspaceUid = getWorkspaceUid(workspacePath);
const isDefault = workspaceUid === 'default';
win.webContents.send('main:workspace-config-updated', workspacePath, workspaceUid, workspaceConfig);
win.webContents.send('main:workspace-config-updated', workspacePath, workspaceUid, {
...workspaceConfig,
type: isDefault ? 'default' : workspaceConfig.type
});
} catch (error) {
console.error('Error handling workspace file change:', error);
}
@@ -123,7 +128,7 @@ class WorkspaceWatcher {
addWatcher(win, workspacePath) {
const workspaceFilePath = path.join(workspacePath, 'workspace.yml');
const environmentsDir = path.join(workspacePath, 'environments');
const workspaceUid = generateUidBasedOnHash(workspacePath);
const workspaceUid = getWorkspaceUid(workspacePath);
if (this.watchers[workspacePath]) {
this.watchers[workspacePath].close();

View File

@@ -3,13 +3,11 @@ const path = require('path');
const fsExtra = require('fs-extra');
const { ipcMain, dialog } = require('electron');
const { createDirectory, sanitizeName } = require('../utils/filesystem');
const { generateUidBasedOnHash } = require('../utils/common');
const yaml = require('js-yaml');
const LastOpenedWorkspaces = require('../store/last-opened-workspaces');
const { defaultWorkspaceManager } = require('../store/default-workspace');
const { globalEnvironmentsManager } = require('../store/workspace-environments');
// Workspace configuration module (includes path and validation utilities)
const {
createWorkspaceConfig,
readWorkspaceConfig,
@@ -22,7 +20,8 @@ const {
getWorkspaceCollections,
normalizeCollectionEntry,
validateWorkspacePath,
validateWorkspaceDirectory
validateWorkspaceDirectory,
getWorkspaceUid
} = require('../utils/workspace-config');
const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
@@ -49,21 +48,25 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
await createDirectory(path.join(dirPath, 'collections'));
const workspaceUid = generateUidBasedOnHash(dirPath);
const workspaceUid = getWorkspaceUid(dirPath);
const isDefault = workspaceUid === 'default';
const workspaceConfig = createWorkspaceConfig(workspaceName);
await writeWorkspaceConfig(dirPath, workspaceConfig);
lastOpenedWorkspaces.add(dirPath);
mainWindow.webContents.send('main:workspace-opened', dirPath, workspaceUid, workspaceConfig);
mainWindow.webContents.send('main:workspace-opened', dirPath, workspaceUid, {
...workspaceConfig,
type: isDefault ? 'default' : workspaceConfig.type
});
if (workspaceWatcher) {
workspaceWatcher.addWatcher(mainWindow, dirPath);
}
return {
workspaceConfig,
workspaceConfig: { ...workspaceConfig, type: isDefault ? 'default' : workspaceConfig.type },
workspaceUid,
workspacePath: dirPath
};
@@ -79,18 +82,20 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
const workspaceConfig = readWorkspaceConfig(workspacePath);
validateWorkspaceConfig(workspaceConfig);
const workspaceUid = generateUidBasedOnHash(workspacePath);
const workspaceUid = getWorkspaceUid(workspacePath);
const isDefault = workspaceUid === 'default';
const configWithType = { ...workspaceConfig, type: isDefault ? 'default' : workspaceConfig.type };
lastOpenedWorkspaces.add(workspacePath);
mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, workspaceConfig);
mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configWithType);
if (workspaceWatcher) {
workspaceWatcher.addWatcher(mainWindow, workspacePath);
}
return {
workspaceConfig,
workspaceConfig: configWithType,
workspaceUid,
workspacePath
};
@@ -117,18 +122,20 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
const workspaceConfig = readWorkspaceConfig(workspacePath);
validateWorkspaceConfig(workspaceConfig);
const workspaceUid = generateUidBasedOnHash(workspacePath);
const workspaceUid = getWorkspaceUid(workspacePath);
const isDefault = workspaceUid === 'default';
const configWithType = { ...workspaceConfig, type: isDefault ? 'default' : workspaceConfig.type };
lastOpenedWorkspaces.add(workspacePath);
mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, workspaceConfig);
mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configWithType);
if (workspaceWatcher) {
workspaceWatcher.addWatcher(mainWindow, workspacePath);
}
return {
workspaceConfig,
workspaceConfig: configWithType,
workspaceUid,
workspacePath
};
@@ -338,8 +345,12 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
const updatedCollections = await addCollectionToWorkspace(workspacePath, normalizedCollection);
const workspaceConfig = readWorkspaceConfig(workspacePath);
const workspaceUid = generateUidBasedOnHash(workspacePath);
mainWindow.webContents.send('main:workspace-config-updated', workspacePath, workspaceUid, workspaceConfig);
const workspaceUid = getWorkspaceUid(workspacePath);
const isDefault = workspaceUid === 'default';
mainWindow.webContents.send('main:workspace-config-updated', workspacePath, workspaceUid, {
...workspaceConfig,
type: isDefault ? 'default' : workspaceConfig.type
});
return updatedCollections;
} catch (error) {
@@ -374,13 +385,16 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
try {
const result = await removeCollectionFromWorkspace(workspacePath, collectionPath);
// Delete collection files if it's a workspace collection
if (result.shouldDeleteFiles && result.removedCollection && fs.existsSync(collectionPath)) {
await fsExtra.remove(collectionPath);
}
const correctWorkspaceUid = generateUidBasedOnHash(workspacePath);
mainWindow.webContents.send('main:workspace-config-updated', workspacePath, correctWorkspaceUid, result.updatedConfig);
const correctWorkspaceUid = getWorkspaceUid(workspacePath);
const isDefault = correctWorkspaceUid === 'default';
mainWindow.webContents.send('main:workspace-config-updated', workspacePath, correctWorkspaceUid, {
...result.updatedConfig,
type: isDefault ? 'default' : result.updatedConfig.type
});
return true;
} catch (error) {
@@ -425,20 +439,12 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
ipcMain.handle('renderer:get-default-workspace', async (event) => {
try {
const result = await defaultWorkspaceManager.ensureDefaultWorkspaceExists();
if (!result) {
return null;
}
const { workspacePath, workspaceUid } = result;
const workspaceFilePath = path.join(workspacePath, 'workspace.yml');
if (!fs.existsSync(workspaceFilePath)) {
return null;
}
const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8');
const workspaceConfig = yaml.load(yamlContent);
const workspaceConfig = readWorkspaceConfig(workspacePath);
return {
workspaceConfig: {
@@ -449,32 +455,30 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
workspacePath
};
} catch (error) {
console.error('Error getting default workspace:', error);
return null;
}
});
ipcMain.on('main:renderer-ready', async (win) => {
try {
// Load default workspace
const defaultResult = await defaultWorkspaceManager.ensureDefaultWorkspaceExists();
if (defaultResult) {
const { workspacePath, workspaceUid } = defaultResult;
const workspaceFilePath = path.join(workspacePath, 'workspace.yml');
const workspaceConfig = readWorkspaceConfig(workspacePath);
if (fs.existsSync(workspaceFilePath)) {
const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8');
const workspaceConfig = yaml.load(yamlContent);
win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, {
...workspaceConfig,
type: 'default'
});
win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, {
...workspaceConfig,
type: 'default'
});
if (workspaceWatcher) {
workspaceWatcher.addWatcher(win, workspacePath);
}
if (workspaceWatcher) {
workspaceWatcher.addWatcher(win, workspacePath);
}
}
// Load other workspaces
const workspacePaths = lastOpenedWorkspaces.getAll();
const invalidPaths = [];
@@ -485,9 +489,13 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
try {
const workspaceConfig = readWorkspaceConfig(workspacePath);
validateWorkspaceConfig(workspaceConfig);
const workspaceUid = generateUidBasedOnHash(workspacePath);
const workspaceUid = getWorkspaceUid(workspacePath);
const isDefault = workspaceUid === 'default';
win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, workspaceConfig);
win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, {
...workspaceConfig,
type: isDefault ? 'default' : workspaceConfig.type
});
if (workspaceWatcher) {
workspaceWatcher.addWatcher(win, workspacePath);

View File

@@ -2,17 +2,18 @@ const fs = require('fs');
const path = require('path');
const { app } = require('electron');
const { generateUidBasedOnHash } = require('../utils/common');
const { writeFile, createDirectory } = require('../utils/filesystem');
const { writeFile } = require('../utils/filesystem');
const { getPreferences, savePreferences } = require('./preferences');
const { globalEnvironmentsStore } = require('./global-environments');
const { generateYamlContent } = require('../utils/workspace-config');
const { generateYamlContent, readWorkspaceConfig, validateWorkspaceConfig } = require('../utils/workspace-config');
const OPENCOLLECTION_VERSION = '1.0.0';
const WORKSPACE_TYPE = 'workspace';
const DEFAULT_WORKSPACE_UID = 'default';
class DefaultWorkspaceManager {
constructor() {
this.defaultWorkspacePath = null;
this.defaultWorkspaceUid = null;
this.initializationPromise = null;
}
@@ -27,16 +28,7 @@ class DefaultWorkspaceManager {
}
getDefaultWorkspaceUid() {
const workspacePath = this.getDefaultWorkspacePath();
if (!workspacePath) {
return null;
}
if (!this.defaultWorkspaceUid) {
this.defaultWorkspaceUid = generateUidBasedOnHash(workspacePath);
}
return this.defaultWorkspaceUid;
return DEFAULT_WORKSPACE_UID;
}
async setDefaultWorkspacePath(workspacePath) {
@@ -48,11 +40,29 @@ class DefaultWorkspaceManager {
await savePreferences(preferences);
this.defaultWorkspacePath = workspacePath;
this.defaultWorkspaceUid = generateUidBasedOnHash(workspacePath);
return workspacePath;
}
isValidDefaultWorkspace(workspacePath) {
if (!workspacePath || !fs.existsSync(workspacePath)) {
return false;
}
const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');
if (!fs.existsSync(workspaceYmlPath)) {
return false;
}
try {
const config = readWorkspaceConfig(workspacePath);
validateWorkspaceConfig(config);
return true;
} catch (error) {
return false;
}
}
async ensureDefaultWorkspaceExists() {
if (this.initializationPromise) {
return this.initializationPromise;
@@ -60,7 +70,8 @@ class DefaultWorkspaceManager {
const existingPath = this.getDefaultWorkspacePath();
if (existingPath && fs.existsSync(existingPath)) {
if (this.isValidDefaultWorkspace(existingPath)) {
this.defaultWorkspacePath = existingPath;
return {
workspacePath: existingPath,
workspaceUid: this.getDefaultWorkspaceUid()
@@ -70,17 +81,15 @@ class DefaultWorkspaceManager {
this.initializationPromise = (async () => {
try {
const shouldMigrate = this.needsMigration();
const newWorkspacePath = await this.initializeDefaultWorkspace(null, { migrateFromPreferences: shouldMigrate });
const workspaceYmlPath = path.join(newWorkspacePath, 'workspace.yml');
if (!fs.existsSync(workspaceYmlPath)) {
this.defaultWorkspacePath = null;
return null;
} else {
return {
workspacePath: newWorkspacePath,
workspaceUid: this.getDefaultWorkspaceUid()
};
}
const newWorkspacePath = await this.initializeDefaultWorkspace({ migrateFromPreferences: shouldMigrate });
return {
workspacePath: newWorkspacePath,
workspaceUid: this.getDefaultWorkspaceUid()
};
} catch (error) {
console.error('Failed to initialize default workspace:', error);
return null;
} finally {
this.initializationPromise = null;
}
@@ -89,35 +98,28 @@ class DefaultWorkspaceManager {
return this.initializationPromise;
}
async initializeDefaultWorkspace(workspacePath = null, options = {}) {
async initializeDefaultWorkspace(options = {}) {
const { migrateFromPreferences = true } = options;
if (!workspacePath) {
const configDir = app.getPath('userData');
const baseWorkspacePath = path.join(configDir, 'default-workspace');
const configDir = app.getPath('userData');
const baseWorkspacePath = path.join(configDir, 'default-workspace');
let finalPath = baseWorkspacePath;
let counter = 1;
while (fs.existsSync(finalPath)) {
finalPath = `${baseWorkspacePath}-${counter}`;
counter++;
}
workspacePath = finalPath;
let workspacePath = baseWorkspacePath;
let counter = 1;
while (fs.existsSync(workspacePath)) {
workspacePath = `${baseWorkspacePath}-${counter}`;
counter++;
}
if (!fs.existsSync(workspacePath)) {
await createDirectory(workspacePath);
}
await createDirectory(path.join(workspacePath, 'collections'));
await createDirectory(path.join(workspacePath, 'environments'));
fs.mkdirSync(workspacePath, { recursive: true });
fs.mkdirSync(path.join(workspacePath, 'collections'), { recursive: true });
fs.mkdirSync(path.join(workspacePath, 'environments'), { recursive: true });
const workspaceConfig = {
opencollection: OPENCOLLECTION_VERSION,
info: {
name: 'My Workspace',
type: 'default'
type: WORKSPACE_TYPE
},
collections: [],
specs: [],

View File

@@ -2,6 +2,7 @@ const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const { writeFile, validateName } = require('./filesystem');
const { generateUidBasedOnHash } = require('./common');
const WORKSPACE_TYPE = 'workspace';
const OPENCOLLECTION_VERSION = '1.0.0';
@@ -354,6 +355,15 @@ const removeApiSpecFromWorkspace = async (workspacePath, apiSpecPath) => {
};
};
const getWorkspaceUid = (workspacePath) => {
const { defaultWorkspaceManager } = require('../store/default-workspace');
const defaultWorkspacePath = defaultWorkspaceManager.getDefaultWorkspacePath();
if (defaultWorkspacePath && path.normalize(workspacePath) === path.normalize(defaultWorkspacePath)) {
return defaultWorkspaceManager.getDefaultWorkspaceUid();
}
return generateUidBasedOnHash(workspacePath);
};
module.exports = {
makeRelativePath,
normalizeCollectionEntry,
@@ -371,5 +381,6 @@ module.exports = {
getWorkspaceApiSpecs,
addApiSpecToWorkspace,
removeApiSpecFromWorkspace,
generateYamlContent
generateYamlContent,
getWorkspaceUid
};

View File

@@ -76,7 +76,8 @@ const createCollection = async (page, collectionName: string, collectionLocation
}
await createCollectionModal.getByRole('button', { name: 'Create', exact: true }).click();
await createCollectionModal.waitFor({ state: 'detached' });
await createCollectionModal.waitFor({ state: 'detached', timeout: 15000 });
await page.waitForTimeout(200);
if (options.openWithSandboxMode != undefined) {
await openCollectionAndAcceptSandbox(page, collectionName, options.openWithSandboxMode);

View File

@@ -0,0 +1,230 @@
import path from 'path';
import fs from 'fs';
import { test, expect } from '../../../playwright';
test.describe('Default Workspace', () => {
test.describe('First Launch', () => {
test('should create default workspace with "My Workspace" name on first launch', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-first-launch');
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
// Verify the workspace name is "My Workspace" in the title bar
const workspaceName = page.locator('.workspace-name');
await expect(workspaceName).toContainText('My Workspace');
await app.context().close();
await app.close();
});
});
test.describe('Persistence', () => {
test('should persist default workspace across app restarts', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-persistence');
// First launch
const app1 = await launchElectronApp({ userDataPath });
const page1 = await app1.firstWindow();
await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page1.locator('.workspace-name')).toContainText('My Workspace');
await app1.close();
// Second launch - same workspace should be loaded
const app2 = await launchElectronApp({ userDataPath });
const page2 = await app2.firstWindow();
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page2.locator('.workspace-name')).toContainText('My Workspace');
await app2.context().close();
await app2.close();
});
});
test.describe('Recovery - Creates NEW workspace (never modifies existing)', () => {
test('should create NEW workspace when existing workspace.yml is deleted', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-recovery-deleted');
// Create a corrupted default workspace BEFORE launching app
const defaultWorkspacePath = path.join(userDataPath, 'default-workspace');
fs.mkdirSync(defaultWorkspacePath, { recursive: true });
fs.mkdirSync(path.join(defaultWorkspacePath, 'collections'), { recursive: true });
// Note: NOT creating workspace.yml - simulating deleted file
// Create preferences pointing to the corrupted workspace
fs.writeFileSync(
path.join(userDataPath, 'preferences.json'),
JSON.stringify({
general: {
defaultWorkspacePath: defaultWorkspacePath
}
})
);
// Launch app - should create NEW workspace
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
// Should show "My Workspace"
await expect(page.locator('.workspace-name')).toContainText('My Workspace');
// Old directory should still exist (never deleted)
expect(fs.existsSync(defaultWorkspacePath)).toBe(true);
// New workspace directory should have been created (default-workspace-1 since default-workspace exists)
const newWorkspacePath = path.join(userDataPath, 'default-workspace-1');
expect(fs.existsSync(newWorkspacePath)).toBe(true);
expect(fs.existsSync(path.join(newWorkspacePath, 'workspace.yml'))).toBe(true);
await app.context().close();
await app.close();
});
test('should create NEW workspace when workspace.yml has invalid YAML', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-recovery-invalid');
// Create workspace with invalid YAML BEFORE launching app
const defaultWorkspacePath = path.join(userDataPath, 'default-workspace');
fs.mkdirSync(defaultWorkspacePath, { recursive: true });
fs.writeFileSync(path.join(defaultWorkspacePath, 'workspace.yml'), 'invalid: yaml: [[[');
// Create preferences pointing to the corrupted workspace
fs.writeFileSync(
path.join(userDataPath, 'preferences.json'),
JSON.stringify({
general: {
defaultWorkspacePath: defaultWorkspacePath
}
})
);
// Launch app - should create NEW workspace
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page.locator('.workspace-name')).toContainText('My Workspace');
// Old corrupted file should still exist (never deleted)
const oldContent = fs.readFileSync(path.join(defaultWorkspacePath, 'workspace.yml'), 'utf8');
expect(oldContent).toContain('invalid: yaml: [[[');
// New workspace should have been created
const newWorkspacePath = path.join(userDataPath, 'default-workspace-1');
expect(fs.existsSync(newWorkspacePath)).toBe(true);
await app.context().close();
await app.close();
});
test('should create NEW workspace when workspace.yml has wrong type', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-recovery-wrong-type');
// Create workspace with wrong type BEFORE launching app
const defaultWorkspacePath = path.join(userDataPath, 'default-workspace');
fs.mkdirSync(defaultWorkspacePath, { recursive: true });
fs.writeFileSync(path.join(defaultWorkspacePath, 'workspace.yml'), `opencollection: 1.0.0
info:
name: My Workspace
type: collection
collections:
specs:
docs: ''
`);
// Create preferences pointing to the invalid workspace
fs.writeFileSync(
path.join(userDataPath, 'preferences.json'),
JSON.stringify({
general: {
defaultWorkspacePath: defaultWorkspacePath
}
})
);
// Launch app
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page.locator('.workspace-name')).toContainText('My Workspace');
// New workspace should have been created
const newWorkspacePath = path.join(userDataPath, 'default-workspace-1');
expect(fs.existsSync(newWorkspacePath)).toBe(true);
await app.context().close();
await app.close();
});
test('should create NEW workspace when directory does not exist', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-recovery-dir-missing');
// Create preferences pointing to non-existent directory
const nonExistentPath = path.join(userDataPath, 'non-existent-workspace');
fs.writeFileSync(
path.join(userDataPath, 'preferences.json'),
JSON.stringify({
general: {
defaultWorkspacePath: nonExistentPath
}
})
);
// Launch app
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page.locator('.workspace-name')).toContainText('My Workspace');
// New workspace should have been created (default-workspace since non-existent doesn't block)
const newWorkspacePath = path.join(userDataPath, 'default-workspace');
expect(fs.existsSync(newWorkspacePath)).toBe(true);
expect(fs.existsSync(path.join(newWorkspacePath, 'workspace.yml'))).toBe(true);
await app.context().close();
await app.close();
});
});
test.describe('UI Behavior', () => {
test('should display default workspace in workspace dropdown', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-ui-dropdown');
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
// Click on workspace name to open dropdown
await page.locator('.workspace-name-container').click();
// Verify default workspace is shown
const workspaceItem = page.locator('.workspace-item, .dropdown-item').filter({ hasText: 'My Workspace' });
await expect(workspaceItem.first()).toBeVisible();
await app.context().close();
await app.close();
});
test('should not show pin button for default workspace', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-ui-no-pin');
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await page.locator('.workspace-name-container').click();
const workspaceItem = page.locator('.workspace-item').filter({ hasText: 'My Workspace' });
// Default workspace should NOT have pin button
await expect(workspaceItem.locator('.pin-btn')).not.toBeVisible();
await app.context().close();
await app.close();
});
});
});

View File

@@ -0,0 +1,208 @@
import path from 'path';
import fs from 'fs';
import { test, expect } from '../../../playwright';
const env = {
DISABLE_SAMPLE_COLLECTION_IMPORT: 'false'
};
test.describe('Default Workspace Migration', () => {
test.describe('Migration from lastOpenedCollections', () => {
test('should migrate collections from lastOpenedCollections to new workspace', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-migration');
await test.step('Setup test collection and preferences', async () => {
const testCollectionPath = path.join(userDataPath, 'my-old-collection');
fs.mkdirSync(testCollectionPath, { recursive: true });
fs.writeFileSync(
path.join(testCollectionPath, 'bruno.json'),
JSON.stringify({
version: '1',
name: 'My Old Collection',
type: 'collection'
})
);
fs.writeFileSync(
path.join(userDataPath, 'preferences.json'),
JSON.stringify({
lastOpenedCollections: [testCollectionPath]
})
);
});
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await test.step('Verify workspace UI', async () => {
await expect(page.locator('.workspace-name')).toContainText('My Workspace');
});
await test.step('Verify workspace filesystem artifacts', async () => {
const workspacePath = path.join(userDataPath, 'default-workspace');
expect(fs.existsSync(workspacePath)).toBe(true);
const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');
expect(fs.existsSync(workspaceYmlPath)).toBe(true);
const workspaceYml = fs.readFileSync(workspaceYmlPath, 'utf8');
expect(workspaceYml).toContain('collections:');
expect(workspaceYml).toContain('my-old-collection');
});
await test.step('Cleanup', async () => {
await app.context().close();
await app.close();
});
});
test('should migrate multiple collections from lastOpenedCollections', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-migration-multiple');
// Create multiple test collections
const collection1Path = path.join(userDataPath, 'collection-1');
const collection2Path = path.join(userDataPath, 'collection-2');
for (const collPath of [collection1Path, collection2Path]) {
fs.mkdirSync(collPath, { recursive: true });
fs.writeFileSync(
path.join(collPath, 'bruno.json'),
JSON.stringify({
version: '1',
name: path.basename(collPath),
type: 'collection'
})
);
}
// Create old-style preferences
fs.writeFileSync(
path.join(userDataPath, 'preferences.json'),
JSON.stringify({
lastOpenedCollections: [collection1Path, collection2Path]
})
);
// Launch app
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page.locator('.workspace-name')).toContainText('My Workspace');
// Verify workspace.yml has both collections
const workspacePath = path.join(userDataPath, 'default-workspace');
const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');
expect(fs.existsSync(workspaceYmlPath)).toBe(true);
const workspaceYml = fs.readFileSync(workspaceYmlPath, 'utf8');
expect(workspaceYml).toContain('collection-1');
expect(workspaceYml).toContain('collection-2');
await app.context().close();
await app.close();
});
});
test.describe('Migration does not affect existing users', () => {
test('should skip sample collection when user has existing collections', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-existing-user');
// Create a test collection (simulating existing user)
const oldCollectionPath = path.join(userDataPath, 'old-user-collection');
fs.mkdirSync(oldCollectionPath, { recursive: true });
fs.writeFileSync(
path.join(oldCollectionPath, 'bruno.json'),
JSON.stringify({
version: '1',
name: 'Old User Collection',
type: 'collection'
})
);
// Create old-style preferences with lastOpenedCollections
fs.writeFileSync(
path.join(userDataPath, 'preferences.json'),
JSON.stringify({
lastOpenedCollections: [oldCollectionPath]
})
);
// Launch app - sample collection should NOT be created (existing user)
const app = await launchElectronApp({ userDataPath, dotEnv: env });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
// Verify default workspace is created
await expect(page.locator('.workspace-name')).toContainText('My Workspace');
// Sample collection should NOT be created (because user has existing collections)
const sampleCollection = page.locator('#sidebar-collection-name').getByText('Sample API Collection');
await expect(sampleCollection).not.toBeVisible();
await app.context().close();
await app.close();
});
});
test.describe('No duplicate workspaces on restart', () => {
test('should reuse existing workspace on subsequent launches', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-reuse');
// First launch - creates workspace
const app1 = await launchElectronApp({ userDataPath });
const page1 = await app1.firstWindow();
await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page1.locator('.workspace-name')).toContainText('My Workspace');
// Verify initial workspace was created
const workspacePath = path.join(userDataPath, 'default-workspace');
expect(fs.existsSync(workspacePath)).toBe(true);
const originalYmlContent = fs.readFileSync(path.join(workspacePath, 'workspace.yml'), 'utf8');
await app1.context().close();
await app1.close();
// Second launch - should reuse existing workspace
const app2 = await launchElectronApp({ userDataPath });
const page2 = await app2.firstWindow();
await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page2.locator('.workspace-name')).toContainText('My Workspace');
// workspace.yml should NOT have been modified
const currentYmlContent = fs.readFileSync(path.join(workspacePath, 'workspace.yml'), 'utf8');
expect(currentYmlContent).toBe(originalYmlContent);
// No new workspace should have been created
expect(fs.existsSync(path.join(userDataPath, 'default-workspace-1'))).toBe(false);
await app2.context().close();
await app2.close();
});
});
test.describe('Clean installation', () => {
test('should create empty workspace on fresh install without old preferences', async ({ launchElectronApp, createTmpDir }) => {
const userDataPath = await createTmpDir('default-workspace-clean');
// Launch with completely empty user data (no preferences file)
const app = await launchElectronApp({ userDataPath });
const page = await app.firstWindow();
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await expect(page.locator('.workspace-name')).toContainText('My Workspace');
// Verify workspace was created
const workspacePath = path.join(userDataPath, 'default-workspace');
expect(fs.existsSync(workspacePath)).toBe(true);
// Verify workspace has empty collections section
const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');
expect(fs.existsSync(workspaceYmlPath)).toBe(true);
const workspaceYml = fs.readFileSync(workspaceYmlPath, 'utf8');
// Collections should be empty (just the key)
expect(workspaceYml).toMatch(/collections:\s*\n/);
await app.context().close();
await app.close();
});
});
});