diff --git a/playwright/index.ts b/playwright/index.ts index 9a98a55b6..95c389439 100644 --- a/playwright/index.ts +++ b/playwright/index.ts @@ -1,4 +1,4 @@ -import { test as baseTest, BrowserContext, ElectronApplication, Page } from '@playwright/test'; +import { test as baseTest, BrowserContext, ElectronApplication, Page, TestInfo } from '@playwright/test'; import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; @@ -25,6 +25,71 @@ async function recursiveCopy(src: string, dest: string) { } } +const TRACING_OPTIONS = { screenshots: true, snapshots: true, sources: true }; + +function isTracingEnabled(testInfo: TestInfo): boolean { + return !!(testInfo as any)._tracing.traceOptions(); +} + +async function usePageWithTracing( + context: BrowserContext, + page: Page, + testInfo: TestInfo, + use: (page: Page) => Promise, + options: { initTracing?: boolean; useChunks?: boolean } = {} +) { + const { initTracing = false, useChunks = true } = options; + + if (!isTracingEnabled(testInfo)) { + await use(page); + return; + } + + const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`); + + if (initTracing) { + try { + await context.tracing.start(TRACING_OPTIONS); + } catch (e) { } + } + + if (useChunks) { + await context.tracing.startChunk(); + await use(page); + await context.tracing.stopChunk({ path: tracePath }); + } else { + await use(page); + await context.tracing.stop({ path: tracePath }); + } + + await testInfo.attach('trace', { path: tracePath }); +} + +/** + * Gracefully close an Electron app by telling it to exit with code 0. + * This avoids the macOS "quit unexpectedly" crash dialog that appears when + * app.context().close() kills subprocesses (renderer/GPU) abruptly before + * the main process can shut down cleanly. + * + * Emits 'before-quit' first so cleanup handlers run (e.g., saving cookies to disk), + * since app.exit() bypasses all lifecycle events. + */ +export async function closeElectronApp(app: ElectronApplication) { + try { + await app.evaluate(({ app }) => { + app.emit('before-quit'); + app.exit(0); + }); + } catch { + // Expected: process exited before the CDP response was sent + } + try { + await app.close(); + } catch { + // Process already exited + } +} + export const test = baseTest.extend< { context: BrowserContext; @@ -113,8 +178,7 @@ export const test = baseTest.extend< return app; }); for (const app of apps) { - await app.context().close(); - await app.close(); + await closeElectronApp(app); } }, { scope: 'worker' } @@ -130,10 +194,9 @@ export const test = baseTest.extend< context: async ({ electronApp }, use, testInfo) => { const context = await electronApp.context(); - const tracingOptions = (testInfo as any)._tracing.traceOptions(); - if (tracingOptions) { + if (isTracingEnabled(testInfo)) { try { - await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); + await context.tracing.start(TRACING_OPTIONS); } catch (e) { } } await use(context); @@ -141,32 +204,14 @@ export const test = baseTest.extend< page: async ({ electronApp, context }, use, testInfo) => { const page = await electronApp.firstWindow(); - const tracingOptions = (testInfo as any)._tracing.traceOptions(); - if (tracingOptions) { - const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`); - await context.tracing.startChunk(); - await use(page); - await context.tracing.stopChunk({ path: tracePath }); - await testInfo.attach('trace', { path: tracePath }); - } else { - await use(page); - } + await usePageWithTracing(context, page, testInfo, use); }, newPage: async ({ launchElectronApp }, use, testInfo) => { const app = await launchElectronApp(); const context = await app.context(); const page = await app.firstWindow(); - const tracingOptions = (testInfo as any)._tracing.traceOptions(); - if (tracingOptions) { - const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`); - await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); - await use(page); - await context.tracing.stop({ path: tracePath }); - await testInfo.attach('trace', { path: tracePath }); - } else { - await use(page); - } + await usePageWithTracing(context, page, testInfo, use, { initTracing: true, useChunks: false }); }, reuseOrLaunchElectronApp: [ @@ -208,8 +253,7 @@ export const test = baseTest.extend< // Clean up all app instances for (const { app } of appInstances) { - await app.context().close(); - await app.close(); + await closeElectronApp(app); } }, @@ -231,19 +275,7 @@ export const test = baseTest.extend< const context = await app.context(); const page = await app.firstWindow(); - const tracingOptions = (testInfo as any)._tracing.traceOptions(); - if (tracingOptions) { - const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`); - try { - await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); - } catch (e) { } - await context.tracing.startChunk(); - await use(page); - await context.tracing.stopChunk({ path: tracePath }); - await testInfo.attach('trace', { path: tracePath }); - } else { - await use(page); - } + await usePageWithTracing(context, page, testInfo, use, { initTracing: true }); } }); diff --git a/tests/collection/draft/draft-values-in-requests.spec.ts b/tests/collection/draft/draft-values-in-requests.spec.ts index 9db7a9a31..5f4e85a35 100644 --- a/tests/collection/draft/draft-values-in-requests.spec.ts +++ b/tests/collection/draft/draft-values-in-requests.spec.ts @@ -23,11 +23,11 @@ test.describe('Draft values are used in requests', () => { const nameEditor = headerRow.locator('.CodeMirror').first(); await nameEditor.click(); - await page.keyboard.type('X-Draft-Header'); + await headerRow.locator('textarea').first().fill('X-Draft-Header'); const valueEditor = headerRow.locator('.CodeMirror').nth(1); await valueEditor.click(); - await page.keyboard.type('draft-value-123'); + await headerRow.locator('textarea').nth(1).fill('draft-value-123'); // Verify draft indicator appears (header is not saved yet) const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) }); @@ -51,11 +51,11 @@ test.describe('Draft values are used in requests', () => { const folderNameEditor = folderHeaderRow.locator('.CodeMirror').first(); await folderNameEditor.click(); - await page.keyboard.type('X-Folder-Draft-Header'); + await folderHeaderRow.locator('textarea').first().fill('X-Folder-Draft-Header'); const folderValueEditor = folderHeaderRow.locator('.CodeMirror').nth(1); await folderValueEditor.click(); - await page.keyboard.type('folder-draft-value-123'); + await folderHeaderRow.locator('textarea').nth(1).fill('folder-draft-value-123'); // Create a request in the collection // Create a new request via collection menu @@ -122,7 +122,7 @@ test.describe('Draft values are used in requests', () => { // Create a new request from collection menu const collection = page.locator('.collection-name').filter({ hasText: collectionName }); await collection.hover(); - await collection.locator('.collection-actions .icon').click(); + await collection.locator('.collection-actions .icon').click({ force: true }); await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); await page.getByTestId('request-name').fill('Test Request'); await page.getByTestId('new-request-url').locator('.CodeMirror').click(); diff --git a/tests/collection/moving-requests/cross-collection-drag-drop-folder.spec.ts b/tests/collection/moving-requests/cross-collection-drag-drop-folder.spec.ts index 2994e7436..b9119475e 100644 --- a/tests/collection/moving-requests/cross-collection-drag-drop-folder.spec.ts +++ b/tests/collection/moving-requests/cross-collection-drag-drop-folder.spec.ts @@ -29,7 +29,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => { // Add a request to the folder to make it more realistic await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).hover(); - await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).locator('.menu-icon').click(); + await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).locator('.menu-icon').click({ force: true }); await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); await page.getByPlaceholder('Request Name').fill('test-request-in-folder'); await page.locator('#new-request-url .CodeMirror').click(); @@ -126,7 +126,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => { // Add a request to the folder to make it more realistic await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).hover(); - await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).locator('.menu-icon').click(); + await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).locator('.menu-icon').click({ force: true }); await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); await page.getByPlaceholder('Request Name').fill('http-request'); await page.locator('#new-request-url .CodeMirror').click(); diff --git a/tests/cookies/cookie-persistence.spec.ts b/tests/cookies/cookie-persistence.spec.ts index beefb3b39..c425fb509 100644 --- a/tests/cookies/cookie-persistence.spec.ts +++ b/tests/cookies/cookie-persistence.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../../playwright'; +import { test, expect, closeElectronApp } from '../../playwright'; test('should persist cookies across app restarts', async ({ createTmpDir, launchElectronApp }) => { // Create a temporary user-data directory so we control where the cookies store file is written. @@ -26,7 +26,7 @@ test('should persist cookies across app restarts', async ({ createTmpDir, launch await expect(page1.getByText('example.com')).toBeVisible(); - await app1.close(); + await closeElectronApp(app1); // Second launch – verify the cookie was persisted and re-loaded const app2 = await launchElectronApp({ userDataPath }); @@ -39,5 +39,5 @@ test('should persist cookies across app restarts', async ({ createTmpDir, launch // The domain we added earlier should still be present. await expect(page2.getByText('example.com')).toBeVisible(); - await app2.close(); + await closeElectronApp(app2); }); diff --git a/tests/cookies/corrupted-passkey.spec.ts b/tests/cookies/corrupted-passkey.spec.ts index dcdd72984..959a966ce 100644 --- a/tests/cookies/corrupted-passkey.spec.ts +++ b/tests/cookies/corrupted-passkey.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../../playwright'; +import { test, expect, closeElectronApp } from '../../playwright'; import * as path from 'path'; import * as fs from 'fs/promises'; @@ -24,7 +24,7 @@ test('should handle corrupted passkey and still display saved cookie list', asyn await expect(page1.getByText('example.com')).toBeVisible(); - await app1.close(); + await closeElectronApp(app1); // 2. Corrupt the encryptedPasskey in cookies.json const cookiesFilePath = path.join(userDataPath, 'cookies.json'); @@ -43,5 +43,5 @@ test('should handle corrupted passkey and still display saved cookie list', asyn // The domain row should still be visible (even if cookie values are blank). await expect(page2.getByText('example.com')).toBeVisible(); - await app2.close(); + await closeElectronApp(app2); }); diff --git a/tests/environments/api-setEnvVar/api-setEnvVar-with-persist.spec.ts b/tests/environments/api-setEnvVar/api-setEnvVar-with-persist.spec.ts index 354825628..2054cf634 100644 --- a/tests/environments/api-setEnvVar/api-setEnvVar-with-persist.spec.ts +++ b/tests/environments/api-setEnvVar/api-setEnvVar-with-persist.spec.ts @@ -38,7 +38,7 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => { await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible(); await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); // we restart the app to confirm that the environment variable is persisted const newApp = await restartApp(); @@ -59,7 +59,7 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => { await expect(newPage.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible(); await newEnvTab.hover(); - await newEnvTab.getByTestId('request-tab-close-icon').click(); + await newEnvTab.getByTestId('request-tab-close-icon').click({ force: true }); // Restore the original Stage.bru file fs.writeFileSync(originalStageBruPath, originalStageBruContent); diff --git a/tests/environments/api-setEnvVar/api-setEnvVar-without-persist.spec.ts b/tests/environments/api-setEnvVar/api-setEnvVar-without-persist.spec.ts index 33ab045db..d5916b3af 100644 --- a/tests/environments/api-setEnvVar/api-setEnvVar-without-persist.spec.ts +++ b/tests/environments/api-setEnvVar/api-setEnvVar-without-persist.spec.ts @@ -28,7 +28,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => { await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible(); await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); // we restart the app to confirm that the environment variable is not persisted const newApp = await restartApp(); @@ -48,7 +48,7 @@ test.describe.serial('bru.setEnvVar(name, value)', () => { await expect(newPage.locator('.table-container tbody')).not.toContainText('token'); await newEnvTab.hover(); - await newEnvTab.getByTestId('request-tab-close-icon').click(); + await newEnvTab.getByTestId('request-tab-close-icon').click({ force: true }); await newPage.close(); }); }); diff --git a/tests/environments/api-setEnvVar/multiple-persist-vars.spec.ts b/tests/environments/api-setEnvVar/multiple-persist-vars.spec.ts index b4454e7cb..9d5d8f598 100644 --- a/tests/environments/api-setEnvVar/multiple-persist-vars.spec.ts +++ b/tests/environments/api-setEnvVar/multiple-persist-vars.spec.ts @@ -27,7 +27,7 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => { } await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); } } catch (error) { // Ignore cleanup errors to avoid masking test failures @@ -85,7 +85,7 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => { await expect(page.getByRole('row', { name: 'multiple-persist-vars-key2' }).getByRole('cell').nth(1)).toBeVisible(); await expect(page.getByRole('row', { name: 'value2' }).getByRole('cell').nth(2)).toBeVisible(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); await test.step('Verify variables are persisted to file', async () => { diff --git a/tests/environments/collection-env-config-selection/collection-env-config-selection.spec.ts b/tests/environments/collection-env-config-selection/collection-env-config-selection.spec.ts index 42948cc77..37a3673a4 100644 --- a/tests/environments/collection-env-config-selection/collection-env-config-selection.spec.ts +++ b/tests/environments/collection-env-config-selection/collection-env-config-selection.spec.ts @@ -35,6 +35,6 @@ test.describe('Collection Environment Configuration Selection Tests', () => { await expect(activeEnvItem).toContainText('prod'); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); }); diff --git a/tests/environments/global-env-config-selection/global-env-config-selection.spec.ts b/tests/environments/global-env-config-selection/global-env-config-selection.spec.ts index d00245c63..15641c13b 100644 --- a/tests/environments/global-env-config-selection/global-env-config-selection.spec.ts +++ b/tests/environments/global-env-config-selection/global-env-config-selection.spec.ts @@ -28,6 +28,6 @@ test.describe('Global Environment Configuration Selection Tests', () => { await expect(activeEnvItem).toContainText(currentEnvName); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); }); diff --git a/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts b/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts index fd18920df..5b3e40e21 100644 --- a/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts +++ b/tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts @@ -47,7 +47,7 @@ test.describe.serial('Collection Environment Import Tests', () => { await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); await test.step('Clean up after test', async () => { @@ -128,7 +128,7 @@ test.describe.serial('Collection Environment Import Tests', () => { await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); await test.step('Clean up after test', async () => { diff --git a/tests/environments/import-environment/bruno-env-import/global-env-import/global-env-import.spec.ts b/tests/environments/import-environment/bruno-env-import/global-env-import/global-env-import.spec.ts index 480af8412..db4d41c56 100644 --- a/tests/environments/import-environment/bruno-env-import/global-env-import/global-env-import.spec.ts +++ b/tests/environments/import-environment/bruno-env-import/global-env-import/global-env-import.spec.ts @@ -62,7 +62,7 @@ test.describe.serial('Global Environment Import Tests', () => { await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); }); @@ -145,7 +145,7 @@ test.describe.serial('Global Environment Import Tests', () => { await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); }); }); diff --git a/tests/environments/import-environment/collection-env-import.spec.ts b/tests/environments/import-environment/collection-env-import.spec.ts index eecdd7104..660f88866 100644 --- a/tests/environments/import-environment/collection-env-import.spec.ts +++ b/tests/environments/import-environment/collection-env-import.spec.ts @@ -62,7 +62,7 @@ test.describe('Collection Environment Import Tests', () => { await expect(page.locator('input[name="5.name"]')).toHaveValue('secretApiToken'); await expect(page.locator('input[name="5.secret"]')).toBeChecked(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); await page.locator('.collection-item-name').first().click(); await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}'); diff --git a/tests/environments/import-environment/global-env-import.spec.ts b/tests/environments/import-environment/global-env-import.spec.ts index 7b0d70eba..55ae79086 100644 --- a/tests/environments/import-environment/global-env-import.spec.ts +++ b/tests/environments/import-environment/global-env-import.spec.ts @@ -57,7 +57,7 @@ test.describe('Global Environment Import Tests', () => { await expect(variablesTable.locator('input[name="5.name"]')).toHaveValue('secretApiToken'); await expect(variablesTable.locator('input[name="5.secret"]')).toBeChecked(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); await page.locator('#collection-environment-test-collection .collection-item-name').first().click(); await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}'); diff --git a/tests/environments/multiline-variables/write-multiline-variable.spec.ts b/tests/environments/multiline-variables/write-multiline-variable.spec.ts index 5e8205668..3cd174796 100644 --- a/tests/environments/multiline-variables/write-multiline-variable.spec.ts +++ b/tests/environments/multiline-variables/write-multiline-variable.spec.ts @@ -33,6 +33,12 @@ test.describe('Multiline Variables - Write Test', () => { await expect(emptyRowNameInput).toBeVisible(); await emptyRowNameInput.fill('multiline_data_json'); + // After filling the name, the table appends a new empty row causing persistent layout shifts. + // Use force:true to bypass Playwright's stability check on the CodeMirror click. + const variableRow = page.locator('tbody tr').filter({ has: page.locator('input[value="multiline_data_json"]') }); + await expect(variableRow).toBeVisible(); + const codeMirror = variableRow.locator('.CodeMirror'); + const jsonValue = `{ "user": { "name": "John Doe", @@ -48,15 +54,13 @@ test.describe('Multiline Variables - Write Test', () => { } }`; - const variableRow = page.locator('tbody tr').filter({ has: page.locator('input[value="multiline_data_json"]') }); - const codeMirror = variableRow.locator('.CodeMirror'); - await codeMirror.click(); + await codeMirror.click({ force: true }); await page.keyboard.insertText(jsonValue); await page.getByTestId('save-env').click(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); await page.getByTestId('send-arrow-icon').click(); diff --git a/tests/environments/update-global-environment-via-script/global-env-update-via-script.spec.ts b/tests/environments/update-global-environment-via-script/global-env-update-via-script.spec.ts index e57834ec8..db84c9910 100644 --- a/tests/environments/update-global-environment-via-script/global-env-update-via-script.spec.ts +++ b/tests/environments/update-global-environment-via-script/global-env-update-via-script.spec.ts @@ -58,7 +58,7 @@ test.describe('Global Environment Variable Update via Script', () => { await test.step('Close the global environment config tab.', async () => { const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' }); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); }); }); diff --git a/tests/global-environments/non-string-values.spec.ts b/tests/global-environments/non-string-values.spec.ts index b759c2edb..2381d3a7d 100644 --- a/tests/global-environments/non-string-values.spec.ts +++ b/tests/global-environments/non-string-values.spec.ts @@ -35,7 +35,7 @@ test.describe('Global Environment Variables - Non-string Values', () => { await page.getByTestId('save-env').click(); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); // Request contains a script that sets the non-string global variables. @@ -124,7 +124,7 @@ test.describe('Global Environment Variables - Non-string Values', () => { const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' }); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); }); }); diff --git a/tests/grpc/method-search/grpc-method-search.spec.ts b/tests/grpc/method-search/grpc-method-search.spec.ts index ffa9c83d1..e2f63c937 100644 --- a/tests/grpc/method-search/grpc-method-search.spec.ts +++ b/tests/grpc/method-search/grpc-method-search.spec.ts @@ -15,7 +15,7 @@ test.describe('Grpc Collection - Method Search Functionality', () => { test.afterEach(async ({ pageWithUserData: page }) => { await test.step('Close the gRPC sayHello tab without saving changes', async () => { - await page.getByRole('tab', { name: 'gRPC sayHello' }).getByTestId('request-tab-close-icon').click(); + await page.getByRole('tab', { name: 'gRPC sayHello' }).getByTestId('request-tab-close-icon').click({ force: true }); await page.getByRole('button', { name: 'Don\'t Save' }).click(); }); }); diff --git a/tests/import/insomnia/import-insomnia-v4-environments.spec.ts b/tests/import/insomnia/import-insomnia-v4-environments.spec.ts index fe564c5da..36921c9c8 100644 --- a/tests/import/insomnia/import-insomnia-v4-environments.spec.ts +++ b/tests/import/insomnia/import-insomnia-v4-environments.spec.ts @@ -200,7 +200,7 @@ test.describe('Import Insomnia v4 Collection - Environment Import', () => { await test.step('Close environment tab', async () => { const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' }); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); }); }); diff --git a/tests/import/insomnia/import-insomnia-v5-environments.spec.ts b/tests/import/insomnia/import-insomnia-v5-environments.spec.ts index 8a3f1e1e9..67372e12d 100644 --- a/tests/import/insomnia/import-insomnia-v5-environments.spec.ts +++ b/tests/import/insomnia/import-insomnia-v5-environments.spec.ts @@ -225,7 +225,7 @@ test.describe('Import Insomnia v5 Collection - Environment Import', () => { await test.step('Close environment tab', async () => { const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' }); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); }); }); diff --git a/tests/onboarding/sample-collection.spec.ts b/tests/onboarding/sample-collection.spec.ts index d520d1944..e7d56de66 100644 --- a/tests/onboarding/sample-collection.spec.ts +++ b/tests/onboarding/sample-collection.spec.ts @@ -1,5 +1,5 @@ import path from 'path'; -import { test, expect, errors } from '../../playwright'; +import { test, expect, errors, closeElectronApp } from '../../playwright'; const env = { DISABLE_SAMPLE_COLLECTION_IMPORT: 'false' @@ -28,7 +28,7 @@ test.describe('Onboarding', () => { await expect(page.locator('#request-url')).toContainText('https://jsonplaceholder.typicode.com/users'); // Clean up - await app.close(); + await closeElectronApp(app); }); test('should not create duplicate collections on subsequent launches', async ({ launchElectronApp, createTmpDir }) => { @@ -51,7 +51,7 @@ test.describe('Onboarding', () => { await expect(page.locator('#request-url')).toContainText('https://jsonplaceholder.typicode.com/users'); // Close the first app instance - await app.close(); + await closeElectronApp(app); // Restart app - should not create sample collection again const newApp = await launchElectronApp({ userDataPath, dotEnv: env }); @@ -71,7 +71,7 @@ test.describe('Onboarding', () => { await expect(newPage.locator('#request-url')).toContainText('https://jsonplaceholder.typicode.com/users'); // Clean up - await newApp.close(); + await closeElectronApp(newApp); }); test('should not recreate sample collection after user deletes it', async ({ launchElectronApp, reuseOrLaunchElectronApp, createTmpDir }) => { diff --git a/tests/preferences/autosave/autosave.spec.ts b/tests/preferences/autosave/autosave.spec.ts index 0d8d5f425..de42204c1 100644 --- a/tests/preferences/autosave/autosave.spec.ts +++ b/tests/preferences/autosave/autosave.spec.ts @@ -52,7 +52,7 @@ test.describe('Autosave', () => { // Close preferences tab using the close icon const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' }); await preferencesTab.hover(); - await preferencesTab.locator('.close-icon').click(); + await preferencesTab.locator('.close-icon').click({ force: true }); // Click on the request to make it active again await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click(); @@ -77,7 +77,7 @@ test.describe('Autosave', () => { // Close and reopen the request tab to verify persistence const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) }); await requestTab.hover(); - await requestTab.getByTestId('request-tab-close-icon').click(); + await requestTab.getByTestId('request-tab-close-icon').click({ force: true }); // Reopen request await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click(); @@ -108,7 +108,7 @@ test.describe('Autosave', () => { // Close preferences tab using the close icon const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' }); await preferencesTab.hover(); - await preferencesTab.locator('.close-icon').click(); + await preferencesTab.locator('.close-icon').click({ force: true }); // Click on the request to make it active again await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click(); @@ -192,7 +192,7 @@ test.describe('Autosave', () => { // Close preferences tab using the close icon const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' }); await preferencesTab.hover(); - await preferencesTab.locator('.close-icon').click(); + await preferencesTab.locator('.close-icon').click({ force: true }); // Click on the request to make it active again await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click(); @@ -207,7 +207,7 @@ test.describe('Autosave', () => { // Close and reopen the request tab to verify persistence const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) }); await requestTab.hover(); - await requestTab.getByTestId('request-tab-close-icon').click(); + await requestTab.getByTestId('request-tab-close-icon').click({ force: true }); // Reopen request await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click(); diff --git a/tests/protobuf/manage-protofile.spec.ts b/tests/protobuf/manage-protofile.spec.ts index a1281b574..9a26cde00 100644 --- a/tests/protobuf/manage-protofile.spec.ts +++ b/tests/protobuf/manage-protofile.spec.ts @@ -108,7 +108,7 @@ test.describe('manage protofile', () => { await method.click(); const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' }); await requestTab.hover(); - await requestTab.getByTestId('request-tab-close-icon').click(); + await requestTab.getByTestId('request-tab-close-icon').click({ force: true }); await page.getByRole('button', { name: 'Don\'t Save' }).click(); }); @@ -136,7 +136,7 @@ test.describe('manage protofile', () => { const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' }); await requestTab.hover(); - await requestTab.getByTestId('request-tab-close-icon').click(); + await requestTab.getByTestId('request-tab-close-icon').click({ force: true }); await page.getByRole('button', { name: 'Don\'t Save' }).click(); }); @@ -180,7 +180,7 @@ test.describe('manage protofile', () => { // Clean up const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' }); await requestTab.hover(); - await requestTab.getByTestId('request-tab-close-icon').click(); + await requestTab.getByTestId('request-tab-close-icon').click({ force: true }); await page.getByRole('button', { name: 'Don\'t Save' }).click(); }); }); diff --git a/tests/request/copy-request/copy-folder.spec.ts b/tests/request/copy-request/copy-folder.spec.ts index 7f1f10d25..2030fb17b 100644 --- a/tests/request/copy-request/copy-folder.spec.ts +++ b/tests/request/copy-request/copy-folder.spec.ts @@ -22,7 +22,7 @@ test.describe('Copy and Paste Folders', () => { // Add a request to the folder await folder.hover(); - await folder.locator('.menu-icon').click(); + await folder.locator('.menu-icon').click({ force: true }); await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); await page.getByPlaceholder('Request Name').fill('request-in-folder'); await page.locator('#new-request-url .CodeMirror').click(); @@ -34,7 +34,7 @@ test.describe('Copy and Paste Folders', () => { // Copy the folder await folder.hover(); - await folder.locator('.menu-icon').click(); + await folder.locator('.menu-icon').click({ force: true }); await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click(); // Paste into the collection root @@ -77,13 +77,13 @@ test.describe('Copy and Paste Folders', () => { // Copy folder-to-copy await folderToCopy.hover(); - await folderToCopy.locator('.menu-icon').click(); + await folderToCopy.locator('.menu-icon').click({ force: true }); await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click(); await folderToCopy.click(); // Paste into target folder await targetFolder.hover(); - await targetFolder.locator('.menu-icon').click(); + await targetFolder.locator('.menu-icon').click({ force: true }); await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click(); // Verify folder was pasted inside target folder diff --git a/tests/request/copy-request/copy-request.spec.ts b/tests/request/copy-request/copy-request.spec.ts index f8a899371..aa580ffdd 100644 --- a/tests/request/copy-request/copy-request.spec.ts +++ b/tests/request/copy-request/copy-request.spec.ts @@ -24,7 +24,7 @@ test.describe('Copy and Paste Requests', () => { // Copy the request const requestItem = page.locator('.collection-item-name').filter({ hasText: 'original-request' }); await requestItem.hover(); - await requestItem.locator('.menu-icon').click(); + await requestItem.locator('.menu-icon').click({ force: true }); await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click(); // Paste into the collection root @@ -48,7 +48,7 @@ test.describe('Copy and Paste Requests', () => { const folder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' }); await folder.click(); await folder.hover(); - await folder.locator('.menu-icon').click(); + await folder.locator('.menu-icon').click({ force: true }); await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click(); await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' })).toHaveCount(3); diff --git a/tests/request/newlines/newlines-persistence.spec.ts b/tests/request/newlines/newlines-persistence.spec.ts index e8ec065aa..f04a8b26e 100644 --- a/tests/request/newlines/newlines-persistence.spec.ts +++ b/tests/request/newlines/newlines-persistence.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../../../playwright'; +import { test, expect, closeElectronApp } from '../../../playwright'; import { createCollection, openCollection } from '../../utils/page'; import { getTableCell } from '../../utils/page/locators'; @@ -39,17 +39,22 @@ test('should persist request with newlines across app restarts', async ({ create await page.getByRole('tab', { name: 'Vars' }).click(); const preReqRow = page.locator('table').first().locator('tbody tr').first(); await getTableCell(preReqRow, 0).getByRole('textbox').fill('preRequestVar'); + // Wait for table to stabilize after fill (new empty row may be appended) + await expect(getTableCell(preReqRow, 0).getByRole('textbox')).toHaveValue('preRequestVar'); await getTableCell(preReqRow, 1).locator('.CodeMirror').click(); await getTableCell(preReqRow, 1).locator('textarea').fill('pre\nRequest\nValue'); const postResRow = page.locator('table').nth(1).locator('tbody tr').first(); await getTableCell(postResRow, 0).getByRole('textbox').fill('postResponseVar'); + // Wait for table to stabilize after fill (new empty row may be appended) + await expect(getTableCell(postResRow, 0).getByRole('textbox')).toHaveValue('postResponseVar'); await getTableCell(postResRow, 1).locator('.CodeMirror').click(); await getTableCell(postResRow, 1).locator('textarea').fill('post\nResponse\nValue'); const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s'; await page.keyboard.press(saveShortcut); - await app1.close(); + await expect(page.getByText('Request saved successfully')).toBeVisible(); + await closeElectronApp(app1); // Verify persistence after restart const app2 = await launchElectronApp({ userDataPath }); @@ -71,5 +76,5 @@ test('should persist request with newlines across app restarts', async ({ create await expect(page2.locator('table').first().locator('tbody tr')).toHaveCount(2); await expect(page2.locator('table').nth(1).locator('tbody tr')).toHaveCount(2); - await app2.close(); + await closeElectronApp(app2); }); diff --git a/tests/request/settings/max-redirects.spec.ts b/tests/request/settings/max-redirects.spec.ts index 9b10145e1..849392237 100644 --- a/tests/request/settings/max-redirects.spec.ts +++ b/tests/request/settings/max-redirects.spec.ts @@ -39,7 +39,7 @@ test.describe('Max Redirects Settings Tests', () => { await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); // Close without saving to avoid modifying the .bru file - await page.locator('.close-icon-container').click(); + await page.locator('.close-icon-container').click({ force: true }); await page.locator('button:has-text("Don\'t Save")').first().click(); }); }); diff --git a/tests/request/settings/no-redirects.spec.ts b/tests/request/settings/no-redirects.spec.ts index db9fc644e..804ad013e 100644 --- a/tests/request/settings/no-redirects.spec.ts +++ b/tests/request/settings/no-redirects.spec.ts @@ -43,7 +43,7 @@ test.describe('No Redirects Settings Tests', () => { await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); // Close without saving to avoid modifying the .bru file - await page.locator('.close-icon-container').click(); + await page.locator('.close-icon-container').click({ force: true }); await page.locator('button:has-text("Don\'t Save")').first().click(); }); }); diff --git a/tests/request/settings/timeout.spec.ts b/tests/request/settings/timeout.spec.ts index b2eee32e9..e30f08105 100644 --- a/tests/request/settings/timeout.spec.ts +++ b/tests/request/settings/timeout.spec.ts @@ -45,7 +45,7 @@ test.describe('Timeout Settings Tests', () => { await expect(responsePane).toContainText('302'); // Close without saving to avoid modifying the .bru file - await page.locator('.close-icon-container').click(); + await page.locator('.close-icon-container').click({ force: true }); await page.locator('button:has-text("Don\'t Save")').first().click(); }); diff --git a/tests/response-examples/menu-operations.spec.ts b/tests/response-examples/menu-operations.spec.ts index f287f280c..df5fc8e23 100644 --- a/tests/response-examples/menu-operations.spec.ts +++ b/tests/response-examples/menu-operations.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from '../../playwright'; import { execSync } from 'child_process'; import path from 'path'; -import { clickResponseAction } from '../utils/page/actions'; +import { clickResponseAction, sendRequest } from '../utils/page/actions'; test.describe.serial('Response Example Menu Operations', () => { test.setTimeout(1 * 60 * 1000); // 1 minute for all tests in this describe block, default is 30 seconds. @@ -17,7 +17,7 @@ test.describe.serial('Response Example Menu Operations', () => { }); await test.step('Create example', async () => { - await page.getByTestId('send-arrow-icon').click(); + await sendRequest(page, 200); await clickResponseAction(page, 'response-bookmark-btn'); await page.getByTestId('create-example-name-input').clear(); await page.getByTestId('create-example-name-input').fill('Example to Clone'); @@ -33,7 +33,7 @@ test.describe.serial('Response Example Menu Operations', () => { await test.step('Clone example', async () => { const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone' }); await exampleRow.hover(); - await exampleRow.locator('.menu-icon').click(); + await exampleRow.locator('.menu-icon').click({ force: true }); await page.getByTestId('response-example-menu-clone').click(); const clonedExampleItem = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone (Copy)' }); @@ -48,7 +48,7 @@ test.describe.serial('Response Example Menu Operations', () => { }); await test.step('Create example to delete', async () => { - await page.getByTestId('send-arrow-icon').click(); + await sendRequest(page, 200); await clickResponseAction(page, 'response-bookmark-btn'); await page.getByTestId('create-example-name-input').clear(); await page.getByTestId('create-example-name-input').fill('Example to Delete'); @@ -65,7 +65,7 @@ test.describe.serial('Response Example Menu Operations', () => { const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Delete' }); await expect(exampleRow).toBeVisible(); await exampleRow.hover(); - await exampleRow.locator('.menu-icon').click(); + await exampleRow.locator('.menu-icon').click({ force: true }); await page.getByTestId('response-example-menu-delete').click(); await expect(page.getByText('Delete Example')).toBeVisible(); @@ -81,7 +81,7 @@ test.describe.serial('Response Example Menu Operations', () => { }); await test.step('Create example to rename', async () => { - await page.getByTestId('send-arrow-icon').click(); + await sendRequest(page, 200); await clickResponseAction(page, 'response-bookmark-btn'); await page.getByTestId('create-example-name-input').clear(); await page.getByTestId('create-example-name-input').fill('Example to Rename'); @@ -98,7 +98,7 @@ test.describe.serial('Response Example Menu Operations', () => { const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Rename' }); await expect(exampleRow).toBeVisible(); await exampleRow.hover(); - await exampleRow.locator('.menu-icon').click(); + await exampleRow.locator('.menu-icon').click({ force: true }); await page.getByTestId('response-example-menu-rename').click(); await expect(page.getByText('Rename Example')).toBeVisible(); const renameExampleNameInput = page.getByTestId('rename-example-name-input'); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index b7b261f4a..46104f099 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -569,7 +569,7 @@ const closeEnvironmentPanel = async (page: Page, type: EnvironmentType = 'collec const tabLabel = type === 'collection' ? 'Environments' : 'Global Environments'; const envTab = page.locator('.request-tab').filter({ hasText: tabLabel }); await envTab.hover(); - await envTab.getByTestId('request-tab-close-icon').click(); + await envTab.getByTestId('request-tab-close-icon').click({ force: true }); }); }; diff --git a/tests/websockets/variable-interpolation/variable-interpolation.spec.ts b/tests/websockets/variable-interpolation/variable-interpolation.spec.ts index 05ba975dd..19454d661 100644 --- a/tests/websockets/variable-interpolation/variable-interpolation.spec.ts +++ b/tests/websockets/variable-interpolation/variable-interpolation.spec.ts @@ -26,9 +26,6 @@ test.describe.serial('WebSocket Variable Interpolation', () => { await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible(); - // Wait a bit for environment to be applied - await page.waitForTimeout(200); - // Connect WebSocket await locators.connectionControls.connect().click(); @@ -52,7 +49,6 @@ test.describe.serial('WebSocket Variable Interpolation', () => { // Click to expand the collection await page.locator('#sidebar-collection-name').filter({ hasText: 'variable-interpolation' }).click(); - await page.waitForTimeout(300); // Open the request await expect(page.getByTitle(BRU_REQ_NAME)).toBeVisible(); @@ -60,7 +56,9 @@ test.describe.serial('WebSocket Variable Interpolation', () => { // Select the test environment (which has data: test-data) await page.locator('div.current-environment').click(); + await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible(); await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click(); + await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible(); // Clear any previous messages await locators.toolbar.clearResponse().click(); @@ -73,9 +71,6 @@ test.describe.serial('WebSocket Variable Interpolation', () => { timeout: MAX_CONNECTION_TIME }); - // Wait a bit for messages to be sent and received (echo server echoes back) - await page.waitForTimeout(1000); - // Verify the sent message contains interpolated value // Should send {"test": "test-data"} (not {"test": "{{data}}"}) const messages = locators.messages(); @@ -83,14 +78,14 @@ test.describe.serial('WebSocket Variable Interpolation', () => { // Find the outgoing message with interpolated content // The echo server will echo back the same message, so we should see it twice const sentMessage = messages.filter({ hasText: 'test-data' }).first(); - await expect(sentMessage).toBeAttached({ timeout: 2000 }); + await expect(sentMessage).toBeAttached({ timeout: MAX_CONNECTION_TIME }); // Verify the message content shows interpolated value, not literal variable - const messageText = await sentMessage.locator('.text-ellipsis').textContent(); - expect(messageText).toContain('test-data'); - expect(messageText).not.toContain('{{data}}'); + const messageContent = sentMessage.locator('.text-ellipsis'); + await expect(messageContent).toContainText('test-data'); + await expect(messageContent).not.toContainText('{{data}}'); // Verify JSON structure is correct - expect(messageText).toMatch(/\{[\s\S]*"test"[\s\S]*"test-data"[\s\S]*\}/); + await expect(messageContent).toContainText('"test"'); }); }); diff --git a/tests/workspace/default-workspace/default-workspace.spec.ts b/tests/workspace/default-workspace/default-workspace.spec.ts index ad1757568..8ca9044e1 100644 --- a/tests/workspace/default-workspace/default-workspace.spec.ts +++ b/tests/workspace/default-workspace/default-workspace.spec.ts @@ -1,6 +1,6 @@ import path from 'path'; import fs from 'fs'; -import { test, expect } from '../../../playwright'; +import { test, expect, closeElectronApp } from '../../../playwright'; test.describe('Default Workspace', () => { test.describe('First Launch', () => { @@ -15,8 +15,7 @@ test.describe('Default Workspace', () => { const workspaceName = page.getByTestId('workspace-name'); await expect(workspaceName).toHaveText('My Workspace'); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); @@ -30,7 +29,7 @@ test.describe('Default Workspace', () => { await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); await expect(page1.getByTestId('workspace-name')).toHaveText('My Workspace'); - await app1.close(); + await closeElectronApp(app1); // Second launch - same workspace should be loaded const app2 = await launchElectronApp({ userDataPath }); @@ -38,8 +37,7 @@ test.describe('Default Workspace', () => { await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace'); - await app2.context().close(); - await app2.close(); + await closeElectronApp(app2); }); }); @@ -79,8 +77,7 @@ test.describe('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(); + await closeElectronApp(app); }); test('should create NEW workspace when workspace.yml has invalid YAML', async ({ launchElectronApp, createTmpDir }) => { @@ -116,8 +113,7 @@ test.describe('Default Workspace', () => { const newWorkspacePath = path.join(userDataPath, 'default-workspace-1'); expect(fs.existsSync(newWorkspacePath)).toBe(true); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should create NEW workspace when workspace.yml has wrong type', async ({ launchElectronApp, createTmpDir }) => { @@ -156,8 +152,7 @@ docs: '' const newWorkspacePath = path.join(userDataPath, 'default-workspace-1'); expect(fs.existsSync(newWorkspacePath)).toBe(true); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should create NEW workspace when directory does not exist', async ({ launchElectronApp, createTmpDir }) => { @@ -186,8 +181,7 @@ docs: '' expect(fs.existsSync(newWorkspacePath)).toBe(true); expect(fs.existsSync(path.join(newWorkspacePath, 'workspace.yml'))).toBe(true); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); @@ -206,8 +200,7 @@ docs: '' const workspaceItem = page.locator('.workspace-item, .dropdown-item').filter({ hasText: 'My Workspace' }); await expect(workspaceItem.first()).toBeVisible(); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should not show pin button for default workspace', async ({ launchElectronApp, createTmpDir }) => { @@ -223,8 +216,7 @@ docs: '' // Default workspace should NOT have pin button await expect(workspaceItem.locator('.pin-btn')).not.toBeVisible(); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); }); diff --git a/tests/workspace/default-workspace/migration.spec.ts b/tests/workspace/default-workspace/migration.spec.ts index 07bf9c906..3617c0ba8 100644 --- a/tests/workspace/default-workspace/migration.spec.ts +++ b/tests/workspace/default-workspace/migration.spec.ts @@ -1,6 +1,6 @@ import path from 'path'; import fs from 'fs'; -import { test, expect } from '../../../playwright'; +import { test, expect, closeElectronApp } from '../../../playwright'; const env = { DISABLE_SAMPLE_COLLECTION_IMPORT: 'false' @@ -50,8 +50,7 @@ test.describe('Default Workspace Migration', () => { }); await test.step('Cleanup', async () => { - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); @@ -97,8 +96,7 @@ test.describe('Default Workspace Migration', () => { expect(workspaceYml).toContain('collection-1'); expect(workspaceYml).toContain('collection-2'); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); @@ -138,8 +136,7 @@ test.describe('Default Workspace Migration', () => { const sampleCollection = page.locator('#sidebar-collection-name').getByText('Sample API Collection'); await expect(sampleCollection).not.toBeVisible(); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); @@ -158,8 +155,7 @@ test.describe('Default Workspace Migration', () => { expect(fs.existsSync(workspacePath)).toBe(true); const originalYmlContent = fs.readFileSync(path.join(workspacePath, 'workspace.yml'), 'utf8'); - await app1.context().close(); - await app1.close(); + await closeElectronApp(app1); // Second launch - should reuse existing workspace const app2 = await launchElectronApp({ userDataPath }); @@ -174,8 +170,7 @@ test.describe('Default Workspace Migration', () => { // 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(); + await closeElectronApp(app2); }); }); @@ -201,8 +196,7 @@ test.describe('Default Workspace Migration', () => { // Collections should be empty (just the key) expect(workspaceYml).toMatch(/collections:\s*\n/); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); }); diff --git a/tests/workspace/default-workspace/recovery-and-backup.spec.ts b/tests/workspace/default-workspace/recovery-and-backup.spec.ts index 2fefc076f..8a20966df 100644 --- a/tests/workspace/default-workspace/recovery-and-backup.spec.ts +++ b/tests/workspace/default-workspace/recovery-and-backup.spec.ts @@ -1,6 +1,6 @@ import path from 'path'; import fs from 'fs'; -import { test, expect } from '../../../playwright'; +import { test, expect, closeElectronApp } from '../../../playwright'; test.describe('Default Workspace Recovery and Backup', () => { test.describe('Global Environments Backup', () => { @@ -61,8 +61,7 @@ test.describe('Default Workspace Recovery and Backup', () => { expect(backup.activeGlobalEnvironmentUid).toBe('env1abcdefghijk123456'); expect(backup.backupDate).toBeDefined(); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should preserve global environments backup across multiple app restarts', async ({ launchElectronApp, createTmpDir }) => { @@ -96,7 +95,7 @@ test.describe('Default Workspace Recovery and Backup', () => { const app1 = await launchElectronApp({ userDataPath }); const page1 = await app1.firstWindow(); await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); - await app1.close(); + await closeElectronApp(app1); // Verify backup exists const backupPath = path.join(userDataPath, 'global-environments-backup.json'); @@ -113,8 +112,7 @@ test.describe('Default Workspace Recovery and Backup', () => { const backupContentAfterSecond = fs.readFileSync(backupPath, 'utf8'); expect(backupContentAfterSecond).toBe(backupContentAfterFirst); - await app2.context().close(); - await app2.close(); + await closeElectronApp(app2); }); }); @@ -140,7 +138,7 @@ test.describe('Default Workspace Recovery and Backup', () => { const app = await launchElectronApp({ userDataPath }); const page = await app.firstWindow(); await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); - await app.close(); + await closeElectronApp(app); // Verify lastOpenedCollections is still in preferences const prefsPath = path.join(userDataPath, 'preferences.json'); @@ -192,8 +190,7 @@ docs: '' const prefs = JSON.parse(fs.readFileSync(path.join(userDataPath, 'preferences.json'), 'utf8')); expect(prefs.preferences?.general?.defaultWorkspacePath).toBe(workspacePath); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should find latest numbered workspace when multiple exist and path not in preferences', async ({ launchElectronApp, createTmpDir }) => { @@ -240,8 +237,7 @@ docs: '' // No new workspace should be created expect(fs.existsSync(path.join(userDataPath, 'default-workspace-3'))).toBe(false); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should skip invalid workspaces and use latest valid one', async ({ launchElectronApp, createTmpDir }) => { @@ -301,8 +297,7 @@ docs: '' const prefs = JSON.parse(fs.readFileSync(path.join(userDataPath, 'preferences.json'), 'utf8')); expect(prefs.preferences?.general?.defaultWorkspacePath).toBe(workspace1); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); @@ -357,8 +352,7 @@ docs: '' const newWorkspace = path.join(userDataPath, 'default-workspace-1'); expect(fs.existsSync(newWorkspace)).toBe(true); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should recover environments from broken workspace to new workspace', async ({ launchElectronApp, createTmpDir }) => { @@ -432,8 +426,7 @@ docs: '' expect(fs.existsSync(path.join(newEnvDir, 'production.yml'))).toBe(true); expect(fs.existsSync(path.join(newEnvDir, 'staging.yml'))).toBe(true); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should use lastOpenedCollections as fallback when workspace config parsing fails', async ({ launchElectronApp, createTmpDir }) => { @@ -473,8 +466,7 @@ docs: '' const workspaceYml = fs.readFileSync(path.join(newWorkspace, 'workspace.yml'), 'utf8'); expect(workspaceYml).toContain('fallback-collection'); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); @@ -531,8 +523,7 @@ docs: '' const createdNew = fs.existsSync(path.join(userDataPath, 'default-workspace-1')); expect(usedExisting || createdNew).toBe(true); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should recover from latest workspace when path does not exist and multiple workspaces exist', async ({ launchElectronApp, createTmpDir }) => { @@ -611,8 +602,7 @@ docs: '' const createdWorkspace2 = fs.existsSync(path.join(userDataPath, 'default-workspace-2')); expect(usedWorkspace1 || createdWorkspace2).toBe(true); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); @@ -637,7 +627,7 @@ docs: '' const workspacePath = path.join(userDataPath, 'default-workspace'); expect(fs.existsSync(workspacePath)).toBe(true); - await app1.close(); + await closeElectronApp(app1); // Now add collection to the workspace const workspaceYmlPath = path.join(workspacePath, 'workspace.yml'); @@ -686,8 +676,7 @@ variables: // Environment should be recovered expect(fs.existsSync(path.join(newWorkspace, 'environments', 'myenv.yml'))).toBe(true); - await app2.context().close(); - await app2.close(); + await closeElectronApp(app2); }); test('should handle workspace deleted between app restarts', async ({ launchElectronApp, createTmpDir }) => { @@ -701,7 +690,7 @@ variables: const workspacePath = path.join(userDataPath, 'default-workspace'); expect(fs.existsSync(workspacePath)).toBe(true); - await app1.close(); + await closeElectronApp(app1); // DELETE the workspace directory fs.rmSync(workspacePath, { recursive: true, force: true }); @@ -716,8 +705,7 @@ variables: expect(fs.existsSync(workspacePath)).toBe(true); expect(fs.existsSync(path.join(workspacePath, 'workspace.yml'))).toBe(true); - await app2.context().close(); - await app2.close(); + await closeElectronApp(app2); }); test('should preserve all data through multiple corruption and recovery cycles', async ({ launchElectronApp, createTmpDir }) => { @@ -741,7 +729,7 @@ variables: const app1 = await launchElectronApp({ userDataPath }); const page1 = await app1.firstWindow(); await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); - await app1.close(); + await closeElectronApp(app1); // Verify workspace-0 created const ws0 = path.join(userDataPath, 'default-workspace'); @@ -764,7 +752,7 @@ variables: [] const app2 = await launchElectronApp({ userDataPath }); const page2 = await app2.firstWindow(); await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); - await app2.close(); + await closeElectronApp(app2); // Verify workspace-1 created with recovered data const ws1 = path.join(userDataPath, 'default-workspace-1'); @@ -790,8 +778,7 @@ variables: [] const ws2Yml = fs.readFileSync(path.join(ws2, 'workspace.yml'), 'utf8'); expect(ws2Yml).toContain('persistent-collection'); - await app3.context().close(); - await app3.close(); + await closeElectronApp(app3); }); }); @@ -818,8 +805,7 @@ variables: [] const newWorkspace = path.join(userDataPath, 'default-workspace-1'); expect(fs.existsSync(newWorkspace)).toBe(true); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should handle missing environments directory during recovery', async ({ launchElectronApp, createTmpDir }) => { @@ -842,8 +828,7 @@ variables: [] // Should not crash expect(fs.existsSync(path.join(userDataPath, 'default-workspace-1'))).toBe(true); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should deduplicate collections between recovered and preference sources', async ({ launchElectronApp, createTmpDir }) => { @@ -885,8 +870,7 @@ variables: [] const collectionEntries = yml.match(/- name:/g); expect(collectionEntries).toHaveLength(1); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); test('should not overwrite recovered environments with global environments of same name', async ({ launchElectronApp, createTmpDir }) => { @@ -943,8 +927,7 @@ variables: expect(envContent).toContain('workspace-value'); expect(envContent).not.toContain('global-value'); - await app.context().close(); - await app.close(); + await closeElectronApp(app); }); }); });