import path from 'path'; import fs from 'fs'; import { test, expect, closeElectronApp, type Page } from '../../playwright'; import { createCollection, createRequest, openRequest, openCollection, switchWorkspace, selectRequestPaneTab, waitForReadyPage } from '../utils/page'; import { buildCommonLocators } from '../utils/page/locators'; /** * Helper: read the snapshot JSON from the user data directory. * electron-store saves it as `ui-state-snapshot.json`. */ const readSnapshot = (userDataPath: string) => { const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json'); if (!fs.existsSync(snapshotPath)) return null; return JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')); }; const clickCollectionsSortAction = async (page: Page, times: number = 1) => { for (let i = 0; i < times; i += 1) { await page.getByTestId('collections-header-actions-menu').click(); await page.getByTestId('collections-header-actions-menu-sort').click(); } }; const expectSidebarCollectionOrder = async (page: Page, expectedNames: string[]) => { const rows = page.getByTestId('sidebar-collection-row'); await expect.poll(async () => { const actualNames: string[] = []; const count = await rows.count(); for (let i = 0; i < count; i += 1) { const name = await rows.nth(i).locator('#sidebar-collection-name').textContent(); if (name) { actualNames.push(name.trim()); } } return actualNames; }).toEqual(expectedNames); }; const expectSnapshotWorkspaceSortings = async (userDataPath: string, expectedSortings: string[]) => { await expect.poll(() => { const snapshot = readSnapshot(userDataPath); const sortings = Array.isArray(snapshot?.workspaces) ? snapshot.workspaces.map((workspace) => workspace?.sorting).filter(Boolean) : []; return sortings.sort(); }).toEqual([...expectedSortings].sort()); }; // ─── Tab Persistence ──────────────────────────────────────────────────────── test.describe('Snapshot: Tab Persistence', () => { test('open tabs are restored after app restart in the same order', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-tabs-order'); const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create collection with two requests and open both', async () => { await createCollection(page, 'TestCol', colPath); await createRequest(page, 'ReqAlpha', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await createRequest(page, 'ReqBeta', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'TestCol', 'ReqAlpha', { persist: true }); await openRequest(page, 'TestCol', 'ReqBeta', { persist: true }); }); await test.step('Close and restart app', async () => { // Wait for debounced snapshot save to flush await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify tabs restored in order', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); // Wait for snapshot hydration to restore tabs await expect(locators.tabs.requestTab('ReqAlpha')).toBeVisible({ timeout: 15000 }); await expect(locators.tabs.requestTab('ReqBeta')).toBeVisible({ timeout: 10000 }); // Verify order: ReqAlpha before ReqBeta const tabs = page2.locator('.request-tab .tab-label'); const tabTexts = await tabs.allTextContents(); const alphaIndex = tabTexts.findIndex((t) => t.includes('ReqAlpha')); const betaIndex = tabTexts.findIndex((t) => t.includes('ReqBeta')); expect(alphaIndex).toBeGreaterThanOrEqual(0); expect(betaIndex).toBeGreaterThan(alphaIndex); await closeElectronApp(app2); }); }); test('active tab is remembered after restart', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-active-tab'); const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create two requests and focus ReqAlpha', async () => { await createCollection(page, 'TestCol', colPath); await createRequest(page, 'ReqAlpha', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await createRequest(page, 'ReqBeta', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'TestCol', 'ReqBeta', { persist: true }); await openRequest(page, 'TestCol', 'ReqAlpha', { persist: true }); const locators = buildCommonLocators(page); await expect(locators.tabs.activeRequestTab()).toContainText('ReqAlpha'); }); await test.step('Close and restart app', async () => { await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify ReqAlpha is the active tab', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.activeRequestTab()).toContainText('ReqAlpha', { timeout: 10000 }); await closeElectronApp(app2); }); }); test('closed tab stays closed after restart', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-closed-tab'); const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create two requests, open both, close one', async () => { await createCollection(page, 'TestCol', colPath); await createRequest(page, 'ReqKeep', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await createRequest(page, 'ReqClose', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'TestCol', 'ReqKeep', { persist: true }); await openRequest(page, 'TestCol', 'ReqClose', { persist: true }); const locators = buildCommonLocators(page); await locators.tabs.closeTab('ReqClose').click({ force: true }); await expect(locators.tabs.requestTab('ReqClose')).not.toBeVisible(); }); await test.step('Close and restart app', async () => { await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify ReqClose is not restored', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.requestTab('ReqKeep')).toBeVisible({ timeout: 10000 }); await expect(locators.tabs.requestTab('ReqClose')).not.toBeVisible(); await closeElectronApp(app2); }); }); test('request pane tab selection persists after restart', async ({ launchElectronApp, createTmpDir }) => { test.setTimeout(60000); const userDataPath = await createTmpDir('snap-pane-tab'); const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create request and switch to Headers tab', async () => { await createCollection(page, 'TestCol', colPath); await createRequest(page, 'Req1', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'TestCol', 'Req1', { persist: true }); await selectRequestPaneTab(page, 'Headers'); }); await test.step('Close and restart app', async () => { await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify Headers tab is still selected', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); // The active collection's tabs should be auto-restored by switchWorkspace await expect(locators.tabs.requestTab('Req1')).toBeVisible({ timeout: 15000 }); // Click the request tab to make it active and show its pane await locators.tabs.requestTab('Req1').click({ force: true }); // The active request pane tab should be Headers const requestPane = page2.locator('.request-pane > .px-4'); await expect(requestPane).toBeVisible({ timeout: 5000 }); const headersTab = requestPane.locator('.tabs').getByRole('tab', { name: 'Headers' }); await expect(headersTab).toHaveClass(/active/, { timeout: 5000 }); await closeElectronApp(app2); }); }); }); // ─── Workspace State ──────────────────────────────────────────────────────── test.describe('Snapshot: Workspace State', () => { test('active workspace is remembered after restart', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-active-ws'); const workspaceBPath = await createTmpDir('workspace-b'); const WORKSPACE_YML = [ 'opencollection: 1.0.0', 'info:', ' name: WorkspaceB', ' type: workspace', 'collections:', 'specs: []', 'docs: \'\'', '' ].join('\n'); fs.writeFileSync(path.join(workspaceBPath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Open WorkspaceB and switch to it', async () => { await app.evaluate( ({ dialog }, targetPath: string) => { (dialog as any).showOpenDialog = () => Promise.resolve({ canceled: false, filePaths: [targetPath] }); }, workspaceBPath ); await page.getByTestId('workspace-menu').click(); await page.locator('.dropdown-item').filter({ hasText: 'Open workspace' }).click(); await expect(page.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); }); await test.step('Close and restart app', async () => { // Wait for debounced snapshot save to flush await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify WorkspaceB is still active', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); await expect(page2.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); await closeElectronApp(app2); }); }); test('workspace collection sorting persists across workspace switches and restart', async ({ launchElectronApp, createTmpDir }) => { // Heaviest restart test in this file (4 collections across 2 workspaces, sort // toggles, multiple snapshot-file polls, then a full restart). It needs the same // extended budget as the other multi-collection restart tests, otherwise it runs // out of time mid-restart on slower runners (e.g. Windows CI, especially when the // job doesn't set CI=true and the default collapses to 30s). test.setTimeout(60000); const userDataPath = await createTmpDir('snap-ws-collection-sorting'); const defaultColZPath = await createTmpDir('default-col-zulu'); const defaultColAPath = await createTmpDir('default-col-alpha'); const secondWorkspacePath = await createTmpDir('workspace-sorting-b'); const secondColMPath = await createTmpDir('ws2-col-middle'); const secondColAPath = await createTmpDir('ws2-col-alpha'); const WORKSPACE_YML = [ 'opencollection: 1.0.0', 'info:', ' name: WorkspaceB', ' type: workspace', 'collections:', 'specs: []', 'docs: \'\'', '' ].join('\n'); fs.writeFileSync(path.join(secondWorkspacePath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create collections in default workspace and set A-Z sort', async () => { await createCollection(page, 'Zulu', defaultColZPath); await createCollection(page, 'Alpha', defaultColAPath); await expectSidebarCollectionOrder(page, ['Zulu', 'Alpha']); await clickCollectionsSortAction(page, 1); await expectSidebarCollectionOrder(page, ['Alpha', 'Zulu']); await expectSnapshotWorkspaceSortings(userDataPath, ['alphabetical']); }); await test.step('Open second workspace and set Z-A sort', async () => { await app.evaluate( ({ dialog }, targetPath: string) => { (dialog as any).showOpenDialog = () => Promise.resolve({ canceled: false, filePaths: [targetPath] }); }, secondWorkspacePath ); await page.getByTestId('workspace-menu').click(); await page.locator('.dropdown-item').filter({ hasText: 'Open workspace' }).click(); await expect(page.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); await createCollection(page, 'Middle', secondColMPath); await createCollection(page, 'AlphaWS2', secondColAPath); await expectSidebarCollectionOrder(page, ['Middle', 'AlphaWS2']); await clickCollectionsSortAction(page, 1); await expectSidebarCollectionOrder(page, ['AlphaWS2', 'Middle']); await clickCollectionsSortAction(page, 1); await expectSidebarCollectionOrder(page, ['Middle', 'AlphaWS2']); await expectSnapshotWorkspaceSortings(userDataPath, ['alphabetical', 'reverseAlphabetical']); }); await test.step('Switch back and forth, verify per-workspace sort restore', async () => { await switchWorkspace(page, 'My Workspace'); await expectSidebarCollectionOrder(page, ['Alpha', 'Zulu']); await switchWorkspace(page, 'WorkspaceB'); await expectSidebarCollectionOrder(page, ['Middle', 'AlphaWS2']); }); await test.step('Restart app and verify per-workspace sort still persists', async () => { await page.waitForTimeout(2000); await closeElectronApp(app); const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); await expect(page2.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); await expectSidebarCollectionOrder(page2, ['Middle', 'AlphaWS2']); await switchWorkspace(page2, 'My Workspace'); await expectSidebarCollectionOrder(page2, ['Alpha', 'Zulu']); await closeElectronApp(app2); }); }); test('each workspace remembers its own active collection', async ({ launchElectronApp, createTmpDir }) => { test.setTimeout(60000); const userDataPath = await createTmpDir('snap-ws-active-col'); const colAPath = await createTmpDir('col-a'); const colBPath = await createTmpDir('col-b'); const workspaceBPath = await createTmpDir('workspace-b'); const WORKSPACE_YML = [ 'opencollection: 1.0.0', 'info:', ' name: WorkspaceB', ' type: workspace', 'collections:', 'specs: []', 'docs: \'\'', '' ].join('\n'); fs.writeFileSync(path.join(workspaceBPath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create ColA with request in default workspace', async () => { await createCollection(page, 'ColA', colAPath); await createRequest(page, 'ReqA', 'ColA', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'ColA', 'ReqA', { persist: true }); }); await test.step('Open WorkspaceB', async () => { await app.evaluate( ({ dialog }, targetPath: string) => { (dialog as any).showOpenDialog = () => Promise.resolve({ canceled: false, filePaths: [targetPath] }); }, workspaceBPath ); await page.getByTestId('workspace-menu').click(); await page.locator('.dropdown-item').filter({ hasText: 'Open workspace' }).click(); await expect(page.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); }); await test.step('Create ColB with request in WorkspaceB', async () => { await createCollection(page, 'ColB', colBPath); await createRequest(page, 'ReqB', 'ColB', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'ColB', 'ReqB', { persist: true }); }); await test.step('Switch back to default workspace and verify ColA tabs restored', async () => { // Wait for snapshot to save before switching await page.waitForTimeout(2000); await switchWorkspace(page, 'My Workspace'); // Wait for collections to load, then click collection to mount and restore tabs const locators = buildCommonLocators(page); await expect(locators.sidebar.collection('ColA')).toBeVisible({ timeout: 10000 }); await openCollection(page, 'ColA'); await expect(locators.tabs.requestTab('ReqA')).toBeVisible({ timeout: 15000 }); }); await test.step('Switch to WorkspaceB and verify ColB tabs restored', async () => { await page.waitForTimeout(2000); await switchWorkspace(page, 'WorkspaceB'); const locators = buildCommonLocators(page); await expect(locators.sidebar.collection('ColB')).toBeVisible({ timeout: 10000 }); await openCollection(page, 'ColB'); await expect(locators.tabs.requestTab('ReqB')).toBeVisible({ timeout: 15000 }); }); await closeElectronApp(app); }); }); // ─── Collection State ─────────────────────────────────────────────────────── test.describe('Snapshot: Collection State', () => { test('collection expanded state persists after restart', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-col-expanded'); const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create collection and open a request (expands it)', async () => { await createCollection(page, 'TestCol', colPath); await createRequest(page, 'Req1', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'TestCol', 'Req1', { persist: true }); // Verify collection is expanded (request is visible in sidebar) const locators = buildCommonLocators(page); await expect(locators.sidebar.request('Req1')).toBeVisible(); }); await test.step('Close and restart app', async () => { await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify collection is still expanded', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); // The active collection should be expanded, showing items in sidebar await expect(locators.sidebar.request('Req1')).toBeVisible({ timeout: 10000 }); await closeElectronApp(app2); }); }); }); // ─── Multi-Workspace Tab Isolation ────────────────────────────────────────── test.describe('Snapshot: Multi-Workspace Tab Isolation', () => { test('tabs from workspace A do not leak into workspace B after restart', async ({ launchElectronApp, createTmpDir }) => { test.setTimeout(60000); const userDataPath = await createTmpDir('snap-tab-isolation'); const colAPath = await createTmpDir('col-a'); const colBPath = await createTmpDir('col-b'); const workspaceBPath = await createTmpDir('workspace-b'); const WORKSPACE_YML = [ 'opencollection: 1.0.0', 'info:', ' name: WorkspaceB', ' type: workspace', 'collections:', 'specs: []', 'docs: \'\'', '' ].join('\n'); fs.writeFileSync(path.join(workspaceBPath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create ReqA in default workspace', async () => { await createCollection(page, 'ColA', colAPath); await createRequest(page, 'ReqA', 'ColA', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'ColA', 'ReqA', { persist: true }); }); await test.step('Switch to WorkspaceB and create ReqB', async () => { await app.evaluate( ({ dialog }, targetPath: string) => { (dialog as any).showOpenDialog = () => Promise.resolve({ canceled: false, filePaths: [targetPath] }); }, workspaceBPath ); await page.getByTestId('workspace-menu').click(); await page.locator('.dropdown-item').filter({ hasText: 'Open workspace' }).click(); await expect(page.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); await createCollection(page, 'ColB', colBPath); await createRequest(page, 'ReqB', 'ColB', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'ColB', 'ReqB', { persist: true }); }); await test.step('Close and restart app', async () => { // Wait for debounced snapshot save to flush await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify WorkspaceB tabs do not show ReqA', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); // App should restore to WorkspaceB (last active) await expect(page2.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); const locators = buildCommonLocators(page2); // Wait for collection to load, then click to bring its tabs into view await expect(locators.sidebar.collection('ColB')).toBeVisible({ timeout: 10000 }); await openCollection(page2, 'ColB'); await expect(locators.tabs.requestTab('ReqB')).toBeVisible({ timeout: 10000 }); await expect(locators.tabs.requestTab('ReqA')).not.toBeVisible(); await test.step('Switch to default workspace and verify ReqA, not ReqB', async () => { await switchWorkspace(page2, 'My Workspace'); // Wait for collections to load in the workspace await expect(locators.sidebar.collection('ColA')).toBeVisible({ timeout: 10000 }); await openCollection(page2, 'ColA'); await expect(locators.tabs.requestTab('ReqA')).toBeVisible({ timeout: 15000 }); await expect(locators.tabs.requestTab('ReqB')).not.toBeVisible(); }); await closeElectronApp(app2); }); }); test('same collection in two workspaces keeps tabs isolated after restart', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-tab-isolation-shared-col'); const sharedColPath = await createTmpDir('shared-col'); const workspaceBPath = await createTmpDir('workspace-b-shared-col'); const WORKSPACE_YML = [ 'opencollection: 1.0.0', 'info:', ' name: WorkspaceB', ' type: workspace', 'collections:', 'specs: []', 'docs: \'\'', '' ].join('\n'); fs.writeFileSync(path.join(workspaceBPath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create shared collection in default workspace and open ReqA', async () => { await createCollection(page, 'SharedCol', sharedColPath); await createRequest(page, 'ReqA', 'SharedCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await createRequest(page, 'ReqB', 'SharedCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'SharedCol', 'ReqA', { persist: true }); }); const sharedCollectionPath = path.join(sharedColPath, 'SharedCol'); await test.step('Open WorkspaceB and add the same collection path', async () => { await app.evaluate( ({ dialog }, targetPath: string) => { (dialog as any).showOpenDialog = () => Promise.resolve({ canceled: false, filePaths: [targetPath] }); }, workspaceBPath ); await page.getByTestId('workspace-menu').click(); await page.locator('.dropdown-item').filter({ hasText: 'Open workspace' }).click(); await expect(page.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); await app.evaluate( ({ dialog }, targetPath: string) => { (dialog as any).showOpenDialog = () => Promise.resolve({ canceled: false, filePaths: [targetPath] }); }, sharedCollectionPath ); await page.getByTestId('collections-header-add-menu').click(); await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Open collection' }).click(); const locators = buildCommonLocators(page); await expect(locators.sidebar.collection('SharedCol')).toBeVisible({ timeout: 10000 }); }); await test.step('In WorkspaceB open ReqB', async () => { await openCollection(page, 'SharedCol'); await openRequest(page, 'SharedCol', 'ReqB', { persist: true }); }); await test.step('Close and restart app', async () => { // snapshot saving is done in the background at 1000ms debounce await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify tab isolation for same collection across workspaces', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); await expect(page2.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); const locators = buildCommonLocators(page2); await expect(locators.sidebar.collection('SharedCol')).toBeVisible({ timeout: 10000 }); await openCollection(page2, 'SharedCol'); await expect(locators.tabs.requestTab('ReqB')).toBeVisible({ timeout: 10000 }); await expect(locators.tabs.requestTab('ReqA')).not.toBeVisible(); await switchWorkspace(page2, 'My Workspace'); await expect(locators.sidebar.collection('SharedCol')).toBeVisible({ timeout: 10000 }); await openCollection(page2, 'SharedCol'); await expect(locators.tabs.requestTab('ReqA')).toBeVisible({ timeout: 15000 }); await expect(locators.tabs.requestTab('ReqB')).not.toBeVisible(); await closeElectronApp(app2); }); }); }); // ─── DevTools State ───────────────────────────────────────────────────────── test.describe('Snapshot: DevTools State', () => { test('devtools open state and active tab persist after restart', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-devtools'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Open devtools and switch to Performance tab', async () => { const devToolsButton = page.locator('button[data-trigger="dev-tools"]'); await devToolsButton.click(); await expect(page.locator('.console-header')).toBeVisible(); const performanceTab = page.locator('.console-tab').filter({ hasText: 'Performance' }); await performanceTab.click(); await expect(performanceTab).toHaveClass(/active/); }); await test.step('Close and restart app', async () => { // Wait for debounced snapshot save to flush await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify devtools is open with Performance tab active', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); // DevTools should be open await expect(page2.locator('.console-header')).toBeVisible({ timeout: 10000 }); // Performance tab should be active const performanceTab = page2.locator('.console-tab').filter({ hasText: 'Performance' }); await expect(performanceTab).toHaveClass(/active/, { timeout: 5000 }); await closeElectronApp(app2); }); }); }); // ─── Edge Cases ───────────────────────────────────────────────────────────── test.describe('Snapshot: Edge Cases', () => { test('fresh app launch with no snapshot file loads cleanly', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-fresh'); // Ensure no snapshot file exists const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json'); if (fs.existsSync(snapshotPath)) { fs.unlinkSync(snapshotPath); } const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); // App should load the default workspace without errors await expect(page.getByTestId('workspace-name')).toBeVisible({ timeout: 10000 }); await closeElectronApp(app); }); test('corrupt snapshot file results in graceful recovery', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-corrupt'); // Write invalid JSON to snapshot file const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json'); fs.writeFileSync(snapshotPath, '{ invalid json !!!', 'utf-8'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); // App should recover and show default workspace await expect(page.getByTestId('workspace-name')).toBeVisible({ timeout: 10000 }); await closeElectronApp(app); }); }); // ─── Snapshot File Structure ──────────────────────────────────────────────── test.describe('Snapshot: File Structure', () => { test('snapshot file uses array-based structure with correct keys', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-structure'); const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create collection and open a request', async () => { await createCollection(page, 'TestCol', colPath); await createRequest(page, 'Req1', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'TestCol', 'Req1', { persist: true }); }); // Give the debounced save time to flush const snapshotPath = path.join(userDataPath, 'ui-state-snapshot.json'); await expect.poll(() => fs.existsSync(snapshotPath)).toBe(true); await test.step('Close app and inspect snapshot file', async () => { await closeElectronApp(app); const snapshot = readSnapshot(userDataPath); expect(snapshot).not.toBeNull(); // Top-level keys should exist expect(snapshot).toHaveProperty('activeWorkspacePath'); expect(snapshot).toHaveProperty('extras'); expect(snapshot).toHaveProperty('extras.devTools'); expect(snapshot).toHaveProperty('workspaces'); expect(snapshot).toHaveProperty('collections'); // workspaces and collections should be arrays expect(Array.isArray(snapshot.workspaces)).toBe(true); expect(Array.isArray(snapshot.collections)).toBe(true); // There should be at least one workspace expect(snapshot.workspaces.length).toBeGreaterThanOrEqual(1); // Workspace should have expected shape const firstWorkspace = snapshot.workspaces[0]; expect(firstWorkspace).toHaveProperty('pathname'); expect(firstWorkspace).toHaveProperty('lastActiveCollectionPathname'); expect(firstWorkspace).toHaveProperty('sorting'); expect(firstWorkspace).toHaveProperty('collections'); expect(Array.isArray(firstWorkspace.collections)).toBe(true); // There should be at least one collection expect(snapshot.collections.length).toBeGreaterThanOrEqual(1); // Collection should have expected shape const firstCollection = snapshot.collections[0]; expect(firstCollection).toHaveProperty('pathname'); expect(firstCollection).toHaveProperty('workspacePathname'); expect(firstCollection).toHaveProperty('environment'); expect(firstCollection).toHaveProperty('environmentPath'); expect(firstCollection).toHaveProperty('selectedEnvironment'); expect(firstCollection).toHaveProperty('isOpen'); expect(firstCollection).toHaveProperty('isMounted'); expect(firstCollection).toHaveProperty('tabs'); expect(Array.isArray(firstCollection.tabs)).toBe(true); expect(firstCollection).toHaveProperty('activeTab'); }); }); }); test.describe('Snapshot: Basic Request Movement', () => { test('requests interactivity is also restored', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-structure'); const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create collection and open a request', async () => { await createCollection(page, 'TestCol', colPath); await createRequest(page, 'Req1', 'TestCol', { url: 'https://echo.usebruno.com', method: 'GET' }); await openRequest(page, 'TestCol', 'Req1', { persist: true }); await selectRequestPaneTab(page, 'Headers'); }); await test.step('Close app and inspect snapshot file', async () => { await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify request pane tabs remain interactive after restore', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.requestTab('Req1')).toBeVisible({ timeout: 15000 }); await locators.tabs.requestTab('Req1').click({ force: true }); await selectRequestPaneTab(page2, 'Headers'); await selectRequestPaneTab(page2, 'Auth'); await selectRequestPaneTab(page2, 'Vars'); await selectRequestPaneTab(page2, 'Tests'); await selectRequestPaneTab(page2, 'Params'); await closeElectronApp(app2); }); }); test('graphql request pane tab interactivity is restored after restart', async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('snap-graphql-interactivity'); const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); const page = await waitForReadyPage(app); await test.step('Create collection and GraphQL request', async () => { await createCollection(page, 'TestCol', colPath); const locators = buildCommonLocators(page); await locators.sidebar.collection('TestCol').hover(); await locators.actions.collectionActions('TestCol').click(); await locators.dropdown.item('New Request').click(); await page.getByTestId('graphql-request').click(); await page.getByTestId('request-name').fill('ReqGraph'); await page.getByTestId('new-request-url').locator('.CodeMirror').click(); await page.keyboard.type('https://echo.usebruno.com/graphql'); await locators.modal.button('Create').click(); await openRequest(page, 'TestCol', 'ReqGraph', { persist: true }); await selectRequestPaneTab(page, 'Headers'); }); await test.step('Close and restart app', async () => { await page.waitForTimeout(2000); await closeElectronApp(app); }); await test.step('Verify GraphQL pane tabs remain interactive', async () => { const app2 = await launchElectronApp({ userDataPath }); const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.requestTab('ReqGraph')).toBeVisible({ timeout: 15000 }); await locators.tabs.requestTab('ReqGraph').click({ force: true }); await selectRequestPaneTab(page2, 'Headers'); await selectRequestPaneTab(page2, 'Auth'); await selectRequestPaneTab(page2, 'Vars'); await selectRequestPaneTab(page2, 'Tests'); await selectRequestPaneTab(page2, 'Query'); await closeElectronApp(app2); }); }); });