mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix(snapshot): normalize renderer:get-last-opened-workspaces output to avoid reactivating a deleted workspace (#8033)
* fix: normalize workspace paths during workspace switch to prevent stale state * chore: test text * tests(snapshot): more workspace coverage
This commit is contained in:
@@ -808,6 +808,7 @@ export const workspaceOpenedEvent = (workspacePath, workspaceUid, workspaceConfi
|
||||
try {
|
||||
const snapshot = await ipcRenderer.invoke('renderer:snapshot:get');
|
||||
const activeWorkspacePath = snapshot?.activeWorkspacePath;
|
||||
const normalizedWorkspacePath = normalizePath(workspacePath || '');
|
||||
|
||||
const currentState = getState();
|
||||
if (!currentState.app.snapshotReady && snapshot?.extras?.devTools) {
|
||||
@@ -822,7 +823,23 @@ export const workspaceOpenedEvent = (workspacePath, workspaceUid, workspaceConfi
|
||||
}
|
||||
|
||||
if (activeWorkspacePath) {
|
||||
shouldSwitch = workspacePath === activeWorkspacePath;
|
||||
const normalizedActiveWorkspacePath = normalizePath(activeWorkspacePath);
|
||||
shouldSwitch = normalizedWorkspacePath === normalizedActiveWorkspacePath;
|
||||
|
||||
// If the snapshot points to a workspace that no longer exists on disk,
|
||||
// fall back to the default workspace instead of leaving stale active state.
|
||||
if (!shouldSwitch && workspaceConfig.type === 'default') {
|
||||
const lastOpenedWorkspacePaths = await ipcRenderer.invoke('renderer:get-last-opened-workspaces').catch(() => []);
|
||||
const normalizedLastOpenedWorkspacePaths = new Set(
|
||||
(Array.isArray(lastOpenedWorkspacePaths) ? lastOpenedWorkspacePaths : [])
|
||||
.map((pathname) => normalizePath(pathname))
|
||||
);
|
||||
const hasActiveWorkspacePath = normalizedLastOpenedWorkspacePaths.has(normalizedActiveWorkspacePath);
|
||||
|
||||
if (!hasActiveWorkspacePath) {
|
||||
shouldSwitch = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
shouldSwitch = !activeWorkspaceUid || workspaceConfig.type === 'default';
|
||||
}
|
||||
|
||||
267
tests/snapshots/workspace.spec.ts
Normal file
267
tests/snapshots/workspace.spec.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import path from 'path';
|
||||
import { test, expect, closeElectronApp } from '../../playwright';
|
||||
import {
|
||||
createCollection,
|
||||
createRequest,
|
||||
openRequest,
|
||||
openWorkspaceFromDialog,
|
||||
waitForReadyPage
|
||||
} from '../utils/page';
|
||||
import fs from 'fs';
|
||||
|
||||
const buildWorkspaceYml = (workspaceName: string) => [
|
||||
'opencollection: 1.0.0',
|
||||
'info:',
|
||||
` name: ${workspaceName}`,
|
||||
' type: workspace',
|
||||
'collections:',
|
||||
'specs: []',
|
||||
'docs: \'\'',
|
||||
''
|
||||
].join('\n');
|
||||
|
||||
test.describe('Snapshot: Deleted Workspace Restoration', () => {
|
||||
test('falls back to default workspace when saved workspace is deleted', async ({ launchElectronApp, createTmpDir }) => {
|
||||
const userDataPath = await createTmpDir('snap-workspace-state');
|
||||
const workspacePath = await createTmpDir('demo-workspace');
|
||||
const defaultCollectionPath = await createTmpDir('default-workspace-col');
|
||||
|
||||
fs.writeFileSync(path.join(workspacePath, 'workspace.yml'), buildWorkspaceYml('Demo Workspace'));
|
||||
|
||||
const app = await launchElectronApp({ userDataPath });
|
||||
const page = await waitForReadyPage(app);
|
||||
|
||||
await test.step('Create a collection in default workspace and mount it', async () => {
|
||||
await createCollection(page, 'Default Workspace Col', defaultCollectionPath);
|
||||
await createRequest(page, 'Default Workspace Req', 'Default Workspace Col', {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET'
|
||||
});
|
||||
await openRequest(page, 'Default Workspace Col', 'Default Workspace Req', { persist: true });
|
||||
await expect(page.getByRole('tab', { name: 'Default Workspace Req' })).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
await test.step('Open Demo Workspace and switch to it', async () => {
|
||||
await openWorkspaceFromDialog(app, page, workspacePath);
|
||||
await expect(page.getByTestId('workspace-name')).toHaveText('Demo Workspace', { timeout: 10000 });
|
||||
});
|
||||
|
||||
await test.step('Close and restart app', async () => {
|
||||
await page.waitForTimeout(2000);
|
||||
await closeElectronApp(app);
|
||||
});
|
||||
|
||||
await test.step('Open after deleting workspace', async () => {
|
||||
await fs.promises.rm(workspacePath, { force: true, recursive: true });
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await waitForReadyPage(app2);
|
||||
await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 10000 });
|
||||
await expect(page2.getByTestId('sidebar-collection-row').filter({ hasText: 'Default Workspace Col' })).toBeVisible({ timeout: 10000 });
|
||||
await openRequest(page2, 'Default Workspace Col', 'Default Workspace Req');
|
||||
await expect(page2.getByRole('tab', { name: 'Default Workspace Req' })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page2.getByTestId('workspace-menu').click();
|
||||
await expect(page2.locator('.workspace-item.active')).toContainText('My Workspace');
|
||||
await expect(page2.locator('.workspace-item').filter({ hasText: 'Demo Workspace' })).toHaveCount(0);
|
||||
await closeElectronApp(app2);
|
||||
});
|
||||
});
|
||||
|
||||
test('falls back to default workspace when saved workspace exists but workspace.yml is missing', async ({ launchElectronApp, createTmpDir }) => {
|
||||
const userDataPath = await createTmpDir('snap-workspace-missing-yml');
|
||||
const workspacePath = await createTmpDir('demo-workspace-missing-yml');
|
||||
const defaultCollectionPath = await createTmpDir('default-workspace-col-missing-yml');
|
||||
|
||||
fs.writeFileSync(path.join(workspacePath, 'workspace.yml'), buildWorkspaceYml('Demo Workspace'));
|
||||
|
||||
const app = await launchElectronApp({ userDataPath });
|
||||
const page = await waitForReadyPage(app);
|
||||
|
||||
await test.step('Create collection and request in default workspace', async () => {
|
||||
await createCollection(page, 'Default Workspace Col', defaultCollectionPath);
|
||||
await createRequest(page, 'Default Workspace Req', 'Default Workspace Col', {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET'
|
||||
});
|
||||
await openRequest(page, 'Default Workspace Col', 'Default Workspace Req', { persist: true });
|
||||
await expect(page.getByRole('tab', { name: 'Default Workspace Req' })).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
await test.step('Switch to demo workspace and restart', async () => {
|
||||
await openWorkspaceFromDialog(app, page, workspacePath);
|
||||
await expect(page.getByTestId('workspace-name')).toHaveText('Demo Workspace', { timeout: 10000 });
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await closeElectronApp(app);
|
||||
});
|
||||
|
||||
await test.step('Delete only workspace.yml and verify fallback', async () => {
|
||||
await fs.promises.unlink(path.join(workspacePath, 'workspace.yml'));
|
||||
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await waitForReadyPage(app2);
|
||||
|
||||
await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 10000 });
|
||||
await expect(page2.getByTestId('sidebar-collection-row').filter({ hasText: 'Default Workspace Col' })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page2.getByTestId('workspace-menu').click();
|
||||
await expect(page2.locator('.workspace-item.active')).toContainText('My Workspace');
|
||||
await expect(page2.locator('.workspace-item').filter({ hasText: 'Demo Workspace' })).toHaveCount(0);
|
||||
|
||||
await closeElectronApp(app2);
|
||||
});
|
||||
});
|
||||
|
||||
test('falls back to default workspace when saved workspace.yml is malformed', async ({ launchElectronApp, createTmpDir }) => {
|
||||
const userDataPath = await createTmpDir('snap-workspace-malformed-yml');
|
||||
const workspacePath = await createTmpDir('demo-workspace-malformed-yml');
|
||||
const defaultCollectionPath = await createTmpDir('default-workspace-col-malformed-yml');
|
||||
|
||||
fs.writeFileSync(path.join(workspacePath, 'workspace.yml'), buildWorkspaceYml('Demo Workspace'));
|
||||
|
||||
const app = await launchElectronApp({ userDataPath });
|
||||
const page = await waitForReadyPage(app);
|
||||
|
||||
await test.step('Create collection and request in default workspace', async () => {
|
||||
await createCollection(page, 'Default Workspace Col', defaultCollectionPath);
|
||||
await createRequest(page, 'Default Workspace Req', 'Default Workspace Col', {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET'
|
||||
});
|
||||
await openRequest(page, 'Default Workspace Col', 'Default Workspace Req', { persist: true });
|
||||
});
|
||||
|
||||
await test.step('Switch to demo workspace and restart', async () => {
|
||||
await openWorkspaceFromDialog(app, page, workspacePath);
|
||||
await expect(page.getByTestId('workspace-name')).toHaveText('Demo Workspace', { timeout: 10000 });
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await closeElectronApp(app);
|
||||
});
|
||||
|
||||
await test.step('Corrupt workspace.yml and verify fallback', async () => {
|
||||
fs.writeFileSync(path.join(workspacePath, 'workspace.yml'), 'invalid: yaml: [[[');
|
||||
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await waitForReadyPage(app2);
|
||||
|
||||
await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 10000 });
|
||||
await openRequest(page2, 'Default Workspace Col', 'Default Workspace Req');
|
||||
await expect(page2.getByRole('tab', { name: 'Default Workspace Req' })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page2.getByTestId('workspace-menu').click();
|
||||
await expect(page2.locator('.workspace-item').filter({ hasText: 'Demo Workspace' })).toHaveCount(0);
|
||||
|
||||
await closeElectronApp(app2);
|
||||
});
|
||||
});
|
||||
|
||||
test('does not restore stale tabs from deleted workspace and remains interactive', async ({ launchElectronApp, createTmpDir }) => {
|
||||
const userDataPath = await createTmpDir('snap-workspace-stale-tabs-deleted');
|
||||
const workspacePath = await createTmpDir('demo-workspace-stale-tabs');
|
||||
const defaultCollectionPath = await createTmpDir('default-workspace-col-stale-tabs');
|
||||
const deletedWorkspaceCollectionPath = await createTmpDir('deleted-workspace-col');
|
||||
|
||||
fs.writeFileSync(path.join(workspacePath, 'workspace.yml'), buildWorkspaceYml('Demo Workspace'));
|
||||
|
||||
const app = await launchElectronApp({ userDataPath });
|
||||
const page = await waitForReadyPage(app);
|
||||
|
||||
await test.step('Create request in default workspace', async () => {
|
||||
await createCollection(page, 'Default Workspace Col', defaultCollectionPath);
|
||||
await createRequest(page, 'Default Workspace Req', 'Default Workspace Col', {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET'
|
||||
});
|
||||
await openRequest(page, 'Default Workspace Col', 'Default Workspace Req', { persist: true });
|
||||
});
|
||||
|
||||
await test.step('Switch to demo workspace and open a request there', async () => {
|
||||
await openWorkspaceFromDialog(app, page, workspacePath);
|
||||
await expect(page.getByTestId('workspace-name')).toHaveText('Demo Workspace', { timeout: 10000 });
|
||||
|
||||
await createCollection(page, 'Deleted Workspace Col', deletedWorkspaceCollectionPath);
|
||||
await createRequest(page, 'Deleted Workspace Req', 'Deleted Workspace Col', {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET'
|
||||
});
|
||||
await openRequest(page, 'Deleted Workspace Col', 'Deleted Workspace Req', { persist: true });
|
||||
await expect(page.getByRole('tab', { name: 'Deleted Workspace Req' })).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
await test.step('Close app, delete active workspace, and verify stale tab is not restored', async () => {
|
||||
await page.waitForTimeout(2000);
|
||||
await closeElectronApp(app);
|
||||
|
||||
await fs.promises.rm(workspacePath, { recursive: true, force: true });
|
||||
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await waitForReadyPage(app2);
|
||||
|
||||
await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 10000 });
|
||||
await expect(page2.getByRole('tab', { name: 'Deleted Workspace Req' })).toHaveCount(0);
|
||||
|
||||
await openRequest(page2, 'Default Workspace Col', 'Default Workspace Req');
|
||||
await expect(page2.getByRole('tab', { name: 'Default Workspace Req' })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await closeElectronApp(app2);
|
||||
});
|
||||
});
|
||||
|
||||
test('falls back when active workspace and active tab belong to malformed workspace snapshot', async ({ launchElectronApp, createTmpDir }) => {
|
||||
const userDataPath = await createTmpDir('snap-workspace-malformed-with-active-tab');
|
||||
const workspacePath = await createTmpDir('demo-workspace-malformed-active-tab');
|
||||
const defaultCollectionPath = await createTmpDir('default-workspace-col-malformed-active-tab');
|
||||
const malformedWorkspaceCollectionPath = await createTmpDir('malformed-workspace-col');
|
||||
|
||||
fs.writeFileSync(path.join(workspacePath, 'workspace.yml'), buildWorkspaceYml('Demo Workspace'));
|
||||
|
||||
const app = await launchElectronApp({ userDataPath });
|
||||
const page = await waitForReadyPage(app);
|
||||
|
||||
await test.step('Create default workspace request', async () => {
|
||||
await createCollection(page, 'Default Workspace Col', defaultCollectionPath);
|
||||
await createRequest(page, 'Default Workspace Req', 'Default Workspace Col', {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET'
|
||||
});
|
||||
await openRequest(page, 'Default Workspace Col', 'Default Workspace Req', { persist: true });
|
||||
});
|
||||
|
||||
await test.step('Switch to demo workspace, create active request, and close app', async () => {
|
||||
await openWorkspaceFromDialog(app, page, workspacePath);
|
||||
await expect(page.getByTestId('workspace-name')).toHaveText('Demo Workspace', { timeout: 10000 });
|
||||
|
||||
await createCollection(page, 'Malformed Workspace Col', malformedWorkspaceCollectionPath);
|
||||
await createRequest(page, 'Malformed Workspace Req', 'Malformed Workspace Col', {
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET'
|
||||
});
|
||||
await openRequest(page, 'Malformed Workspace Col', 'Malformed Workspace Req', { persist: true });
|
||||
await expect(page.getByRole('tab', { name: 'Malformed Workspace Req' })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await closeElectronApp(app);
|
||||
});
|
||||
|
||||
await test.step('Corrupt workspace config and verify app recovers to default workspace', async () => {
|
||||
fs.writeFileSync(path.join(workspacePath, 'workspace.yml'), 'broken: [[[');
|
||||
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await waitForReadyPage(app2);
|
||||
|
||||
await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 10000 });
|
||||
await expect(page2.getByRole('tab', { name: 'Malformed Workspace Req' })).toHaveCount(0);
|
||||
|
||||
await page2.getByTestId('workspace-menu').click();
|
||||
await expect(page2.locator('.workspace-item.active')).toContainText('My Workspace');
|
||||
await expect(page2.locator('.workspace-item').filter({ hasText: 'Demo Workspace' })).toHaveCount(0);
|
||||
await page2.keyboard.press('Escape');
|
||||
|
||||
await openRequest(page2, 'Default Workspace Col', 'Default Workspace Req');
|
||||
await expect(page2.getByRole('tab', { name: 'Default Workspace Req' })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await closeElectronApp(app2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1428,6 +1428,23 @@ const openExampleFromSidebar = async (page: Page, requestName: string, exampleNa
|
||||
await exampleRow.click();
|
||||
};
|
||||
|
||||
type DialogOptions = {
|
||||
showOpenDialog: () => Promise<{ canceled: boolean; filePaths: string[] }>;
|
||||
};
|
||||
|
||||
const openWorkspaceFromDialog = async (app: any, page: any, targetPath: string) => {
|
||||
await app.evaluate(
|
||||
({ dialog }: { dialog: DialogOptions }, workspacePath: string) => {
|
||||
dialog.showOpenDialog = () =>
|
||||
Promise.resolve({ canceled: false, filePaths: [workspacePath] });
|
||||
},
|
||||
targetPath
|
||||
);
|
||||
|
||||
await page.getByTestId('workspace-menu').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Open workspace' }).click();
|
||||
};
|
||||
|
||||
export {
|
||||
waitForReadyPage,
|
||||
closeAllCollections,
|
||||
@@ -1479,7 +1496,8 @@ export {
|
||||
typeIntoField,
|
||||
readField,
|
||||
createExampleFromSidebar,
|
||||
openExampleFromSidebar
|
||||
openExampleFromSidebar,
|
||||
openWorkspaceFromDialog
|
||||
};
|
||||
|
||||
export type { SandboxMode, EnvironmentType, EnvironmentVariable, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions, CreateTransientRequestOptions, AssertionInput };
|
||||
|
||||
Reference in New Issue
Block a user