mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
feat(playwright): isolate collections by copying to a temp directory before launch
This commit is contained in:
@@ -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<string, string>
|
||||
) {
|
||||
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<string>
|
||||
) {
|
||||
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<string, string>(); // 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<void>
|
||||
) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user