diff --git a/playwright/index.ts b/playwright/index.ts index 9a98a55b6..8eb41f867 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'; @@ -19,12 +19,107 @@ async function recursiveCopy(src: string, dest: string) { for (const file of files) { if (!file.isFile()) continue; - const fullPath = path.join(src, file.name); - const fullDestPath = path.join(dest, file.name); + const parentDir = file.parentPath || (file as any).path; + const fullPath = path.join(parentDir, file.name); + const relativePath = path.relative(src, fullPath); + const fullDestPath = path.join(dest, relativePath); + await fs.promises.mkdir(path.dirname(fullDestPath), { recursive: true }); await fs.promises.copyFile(fullPath, fullDestPath); } } +async function rewriteCollectionPaths( + userDataPath: string, + fileName: string, + pathKey: string, + pathMap: Map +) { + const filePath = path.join(userDataPath, fileName); + if (!await existsAsync(filePath)) return; + + const content = await fs.promises.readFile(filePath, 'utf-8'); + const data = JSON.parse(content); + if (!Array.isArray(data.collections)) return; + + let changed = false; + for (const entry of data.collections) { + const mapped = pathMap.get(entry[pathKey]); + if (mapped) { + entry[pathKey] = mapped; + changed = true; + } + } + + if (changed) { + await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8'); + } +} + +async function isolateCollections( + userDataPath: string, + createTmpDir: (tag?: string) => Promise +) { + const prefsPath = path.join(userDataPath, 'preferences.json'); + if (!await existsAsync(prefsPath)) return; + + const content = await fs.promises.readFile(prefsPath, 'utf-8'); + const prefs = JSON.parse(content); + + if (Array.isArray(prefs.lastOpenedCollections) && prefs.lastOpenedCollections.length > 0) { + const pathMap = new Map(); // original -> isolated + const newPaths: string[] = []; + for (const collPath of prefs.lastOpenedCollections) { + if (!await existsAsync(collPath)) { + newPaths.push(collPath); + continue; + } + + // Copy the parent directory of the collection so that relative + // paths (e.g. ../protos/, ../client.pfx) still resolve correctly. + // The collection subdirectory itself becomes the isolated copy. + const parentDir = path.dirname(collPath); + const collBaseName = path.basename(collPath); + const tmpParentDir = await createTmpDir('collection'); + await recursiveCopy(parentDir, tmpParentDir); + const tmpCollDir = path.join(tmpParentDir, collBaseName); + + pathMap.set(collPath, tmpCollDir); + newPaths.push(tmpCollDir); + } + prefs.lastOpenedCollections = newPaths; + await fs.promises.writeFile(prefsPath, JSON.stringify(prefs, null, 2), 'utf-8'); + + // Rewrite collection paths in user-data JSON files that reference + // collections by their original filesystem path so lookups still + // match after isolation. + if (pathMap.size > 0) { + await rewriteCollectionPaths(userDataPath, 'collection-security.json', 'path', pathMap); + await rewriteCollectionPaths(userDataPath, 'ui-state-snapshot.json', 'pathname', pathMap); + } + } +} + +async function withTracing( + context: BrowserContext, + page: Page, + testInfo: TestInfo, + use: (page: Page) => Promise +) { + 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); + } +} + export const test = baseTest.extend< { context: BrowserContext; @@ -82,6 +177,10 @@ export const test = baseTest.extend< }); await fs.promises.writeFile(path.join(userDataPath, file), content, 'utf-8'); } + + // Copy referenced collections to temp dirs so parallel workers + // never share the same filesystem paths + await isolateCollections(userDataPath, createTmpDir); } const app = await playwright._electron.launch({ @@ -128,45 +227,21 @@ export const test = baseTest.extend< { scope: 'worker' } ], - context: async ({ electronApp }, use, testInfo) => { + context: async ({ electronApp }, use) => { const context = await electronApp.context(); - const tracingOptions = (testInfo as any)._tracing.traceOptions(); - if (tracingOptions) { - try { - await context.tracing.start({ screenshots: true, snapshots: true, sources: true }); - } catch (e) { } - } await use(context); }, 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 withTracing(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 withTracing(context, page, testInfo, use); }, reuseOrLaunchElectronApp: [ @@ -231,19 +306,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 withTracing(context, page, testInfo, use); } }); diff --git a/tests/collection/close-all-collections/close-all-collections.spec.ts b/tests/collection/close-all-collections/close-all-collections.spec.ts index 94f878199..faa09ebaf 100644 --- a/tests/collection/close-all-collections/close-all-collections.spec.ts +++ b/tests/collection/close-all-collections/close-all-collections.spec.ts @@ -1,7 +1,5 @@ -import { execSync } from 'child_process'; import { test, expect } from '../../../playwright'; import { Page, ElectronApplication } from '@playwright/test'; -import path from 'path'; import { openCollection } from '../../utils/page/actions'; import { buildCommonLocators } from '../../utils/page/locators'; @@ -20,11 +18,6 @@ const restartAppAndGetLocators = async (restartApp: (options?: { initUserDataPat // The CollectionsHeader component (with collections-header-actions-menu-close-all) is not rendered in workspace mode // The "Remove from workspace" flow is different from the old "Close collection" flow test.describe.skip('Close All Collections', () => { - test.afterAll(async () => { - // Reset the request file to the original state after saving changes - execSync(`git checkout -- "${path.join(__dirname, 'fixtures', 'collections', 'collection 1', 'test-request.bru')}"`); - }); - test('should show/hide close all icon based on hover state', async ({ pageWithUserData: page }) => { const locators = buildCommonLocators(page); 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..0e991eaab 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 @@ -1,19 +1,13 @@ import { test, expect } from '../../../../../playwright'; import path from 'path'; -import fs from 'fs'; test.describe.serial('Collection Environment Import Tests', () => { - test('should import single collection environment', async ({ pageWithUserData: page }) => { + test('should import single collection environment', async ({ restartApp }) => { + const app = await restartApp(); + const page = await app.firstWindow(); const singleEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/local.json'); - const collectionPath = path.join(__dirname, 'fixtures/collection'); - const environmentsPath = path.join(collectionPath, 'environments'); - - await test.step('Clean up existing environments and open collection', async () => { - // Clean up any existing environments folder before test - if (fs.existsSync(environmentsPath)) { - fs.rmSync(environmentsPath, { recursive: true, force: true }); - } + await test.step('Open collection', async () => { // Open the collection from sidebar await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click(); }); @@ -49,26 +43,14 @@ test.describe.serial('Collection Environment Import Tests', () => { await envTab.hover(); await envTab.getByTestId('request-tab-close-icon').click(); }); - - await test.step('Clean up after test', async () => { - // Clean up any existing environments folder before test - if (fs.existsSync(environmentsPath)) { - fs.rmSync(environmentsPath, { recursive: true, force: true }); - } - }); }); - test('should import multiple collection environments', async ({ pageWithUserData: page }) => { + test('should import multiple collection environments', async ({ restartApp }) => { + const app = await restartApp(); + const page = await app.firstWindow(); const multiEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/bruno-collection-environments.json'); - const collectionPath = path.join(__dirname, 'fixtures/collection'); - const environmentsPath = path.join(collectionPath, 'environments'); - - await test.step('Clean up existing environments and open collection', async () => { - // Clean up any existing environments folder before test - if (fs.existsSync(environmentsPath)) { - fs.rmSync(environmentsPath, { recursive: true, force: true }); - } + await test.step('Open collection', async () => { // Open the collection from sidebar await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click(); }); @@ -130,12 +112,5 @@ test.describe.serial('Collection Environment Import Tests', () => { await envTab.hover(); await envTab.getByTestId('request-tab-close-icon').click(); }); - - await test.step('Clean up after test', async () => { - // Clean up any existing environments folder before test - if (fs.existsSync(environmentsPath)) { - fs.rmSync(environmentsPath, { recursive: true, force: true }); - } - }); }); }); diff --git a/tests/protobuf/manage-protofile.spec.ts b/tests/protobuf/manage-protofile.spec.ts index e96d17019..eb96c75ea 100644 --- a/tests/protobuf/manage-protofile.spec.ts +++ b/tests/protobuf/manage-protofile.spec.ts @@ -1,27 +1,6 @@ -import path from 'path'; import { test, expect } from '../../playwright'; -import { closeAllCollections } from '../utils/page'; -import fs from 'fs'; - -const COLLECTION_PATH = path.join(__dirname, 'collection', 'bruno.json'); -const BACKUP_PATH = path.join(__dirname, 'collection', 'bruno.json.backup'); -import { execSync } from 'child_process'; test.describe('manage protofile', () => { - test.beforeAll(async () => { - // Backup original file - if (fs.existsSync(COLLECTION_PATH)) { - fs.copyFileSync(COLLECTION_PATH, BACKUP_PATH); - } - }); - - test.afterAll(async ({ pageWithUserData: page }) => { - // Close all collections - await closeAllCollections(page); - // Reset the collection request file to the original state - execSync(`git checkout -- ${path.join(__dirname, 'collection', 'bruno.json')}`); - }); - test('protofiles, import paths from bruno.json are visible in the protobuf settings', async ({ pageWithUserData: page }) => { await page.locator('#sidebar-collection-name').filter({ hasText: 'Grpcbin' }).click(); diff --git a/tests/request/headers/header-validation.spec.ts b/tests/request/headers/header-validation.spec.ts index 4c5c62fab..9bf2f8975 100644 --- a/tests/request/headers/header-validation.spec.ts +++ b/tests/request/headers/header-validation.spec.ts @@ -51,9 +51,8 @@ test.describe.serial('Header Validation', () => { const headerRow = page.locator('table tbody tr').first(); const nameCell = getTableCell(headerRow, 0); - // Clear and enter a valid header name - await nameCell.locator('.CodeMirror').click(); - await page.keyboard.press('Meta+a'); + // Clear and enter a valid header name - use triple-click to select all (works cross-platform) + await nameCell.locator('.CodeMirror').click({ clickCount: 3 }); await nameCell.locator('textarea').fill('Valid-Header'); // Verify the error icon is not visible diff --git a/tests/response-examples/create-example.spec.ts b/tests/response-examples/create-example.spec.ts index b2d56bdec..395c40da5 100644 --- a/tests/response-examples/create-example.spec.ts +++ b/tests/response-examples/create-example.spec.ts @@ -1,14 +1,7 @@ -import { execSync } from 'child_process'; import { test, expect } from '../../playwright'; -import path from 'path'; import { clickResponseAction } from '../utils/page/actions'; test.describe.serial('Create and Delete Response Examples', () => { - test.afterAll(async () => { - // Reset the collection request file to the original state - execSync(`git checkout -- ${path.join(__dirname, 'fixtures', 'collection', 'create-example.bru')}`); - }); - test('should create a response example from response bookmark', async ({ pageWithUserData: page }) => { await test.step('Open collection and request', async () => { await page.locator('#sidebar-collection-name').filter({ hasText: 'collection' }).click(); diff --git a/tests/response-examples/edit-example.spec.ts b/tests/response-examples/edit-example.spec.ts index 41abc3b9d..cccf92875 100644 --- a/tests/response-examples/edit-example.spec.ts +++ b/tests/response-examples/edit-example.spec.ts @@ -1,14 +1,7 @@ import { test, expect } from '../../playwright'; -import { execSync } from 'child_process'; -import path from 'path'; import { clickResponseAction } from '../utils/page/actions'; test.describe.serial('Edit Response Examples', () => { - test.afterAll(async () => { - // Reset the collection request file to the original state - execSync(`git checkout -- ${path.join(__dirname, 'fixtures', 'collection', 'edit-example.bru')}`); - }); - test('should enter edit mode and show editable fields when edit button is clicked', async ({ pageWithUserData: page }) => { await test.step('Open collection and request', async () => { await page.locator('#sidebar-collection-name').getByText('collection').click(); diff --git a/tests/response-examples/menu-operations.spec.ts b/tests/response-examples/menu-operations.spec.ts index f287f280c..0e17bf27b 100644 --- a/tests/response-examples/menu-operations.spec.ts +++ b/tests/response-examples/menu-operations.spec.ts @@ -1,14 +1,8 @@ import { test, expect } from '../../playwright'; -import { execSync } from 'child_process'; -import path from 'path'; import { clickResponseAction } 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. - test.afterAll(async () => { - // Reset the collection request file to the original state - execSync(`git checkout -- ${path.join(__dirname, 'fixtures', 'collection', 'menu-operations.bru')}`); - }); test('should clone a response example via three dots menu', async ({ pageWithUserData: page }) => { await test.step('Open collection and request', async () => {