mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-02 08:58:32 +00:00
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:
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
230
tests/workspace/default-workspace/default-workspace.spec.ts
Normal file
230
tests/workspace/default-workspace/default-workspace.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
208
tests/workspace/default-workspace/migration.spec.ts
Normal file
208
tests/workspace/default-workspace/migration.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user