Skip to content

Commit 7a60e3f

Browse files
committed
Added Playwright test for bruno-testbench, few sanity tests and improvements
- Trace will capture snapshots now - Added ability to add init Electron user-data, preferences and other app settings. - Improved test Fixtures - Use tempdir for Electron user-data - Ability to reuse app instance for a given init user-data by placing them in a folder(`pageWithUserData` Fixture) - Ability to create tests with fresh user-data(`newPage` Fixture) - Improved logging - Improved the env vars to customize the Electron user-data-path
1 parent 9f044c4 commit 7a60e3f

File tree

10 files changed

+263
-35
lines changed

10 files changed

+263
-35
lines changed

contributing.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,13 @@ npm run dev
9999
```
100100

101101
#### Customize Electron `userData` path
102-
If `ELECTRON_APP_NAME` env-variable is present and its development mode, then the `appName` and `userData` path is modified accordingly.
102+
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
103103

104104
e.g.
105105
```sh
106-
ELECTRON_APP_NAME=bruno-dev npm run dev:electron
106+
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
107107
```
108-
109-
> This doesn't change the name of the window or the names in lot of other places, only the name used by Electron internally.
108+
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
110109

111110
### Troubleshooting
112111

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test, expect } from '../../playwright';
2+
3+
test('Check if the logo on top left is visible', async ({ page }) => {
4+
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
5+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { test, expect } from '../../playwright';
2+
3+
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
4+
await page.getByLabel('Create Collection').click();
5+
await page.getByLabel('Name').click();
6+
await page.getByLabel('Name').fill('test-collection');
7+
await page.getByLabel('Name').press('Tab');
8+
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
9+
await page.getByRole('button', { name: 'Create', exact: true }).click();
10+
await page.getByText('test-collection').click();
11+
await page.getByLabel('Safe ModeBETA').check();
12+
await page.getByRole('button', { name: 'Save' }).click();
13+
await page.locator('#create-new-tab').getByRole('img').click();
14+
await page.getByPlaceholder('Request Name').fill('r1');
15+
await page.getByPlaceholder('Request URL').click();
16+
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
17+
await page.getByRole('button', { name: 'Create' }).click();
18+
await page.locator('pre').filter({ hasText: 'http://localhost:' }).click();
19+
await page.locator('textarea').fill('/ping');
20+
await page.locator('#send-request').getByRole('img').nth(2).click();
21+
await expect(page.getByRole('main')).toContainText('200 OK');
22+
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
23+
await page.getByRole('button', { name: 'Save', exact: true }).click();
24+
await page.getByText('GETr1').click();
25+
await page.getByRole('button', { name: 'Clear response' }).click();
26+
await page.locator('body').press('ControlOrMeta+Enter');
27+
await expect(page.getByRole('main')).toContainText('200 OK');
28+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"maximized": true,
3+
"lastOpenedCollections": ["{{projectRoot}}/packages/bruno-tests/collection"]
4+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { test, expect } from '../../playwright';
2+
3+
test.describe.parallel('Run Testbench Requests', () => {
4+
test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {
5+
test.setTimeout(2 * 60 * 1000);
6+
await page.getByText('bruno-testbench').click();
7+
await page.getByLabel('Developer Mode(use only if').check();
8+
await page.getByRole('button', { name: 'Save' }).click();
9+
await page.locator('.environment-selector').nth(1).click();
10+
await page.locator('.dropdown-item').getByText('Prod').click();
11+
await page.locator('.collection-actions').hover();
12+
await page.locator('.collection-actions .icon').click();
13+
await page.getByText('Run', { exact: true }).click();
14+
await page.getByRole('button', { name: 'Run Collection' }).click();
15+
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
16+
const result = await page.getByText('Total Requests: ').innerText();
17+
const [totalRequests, passed, failed, skipped] = result
18+
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
19+
.slice(1);
20+
await expect(parseInt(failed)).toBe(0);
21+
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
22+
});
23+
24+
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
25+
test.setTimeout(2 * 60 * 1000);
26+
await page.getByText('bruno-testbench').click();
27+
await page.getByLabel('Safe ModeBETA').check();
28+
await page.getByRole('button', { name: 'Save' }).click();
29+
await page.locator('.environment-selector').nth(1).click();
30+
await page.locator('.dropdown-item').getByText('Prod').click();
31+
await page.locator('.collection-actions').hover();
32+
await page.locator('.collection-actions .icon').click();
33+
await page.getByText('Run', { exact: true }).click();
34+
await page.getByRole('button', { name: 'Run Collection' }).click();
35+
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
36+
const result = await page.getByText('Total Requests: ').innerText();
37+
const [totalRequests, passed, failed, skipped] = result
38+
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
39+
.slice(1);
40+
await expect(parseInt(failed)).toBe(0);
41+
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
42+
});
43+
});

e2e-tests/test-app-start.spec.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/bruno-electron/src/index.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,11 @@ const { format } = require('url');
1414
const { BrowserWindow, app, session, Menu, ipcMain } = require('electron');
1515
const { setContentSecurityPolicy } = require('electron-util');
1616

17-
if (isDev && process.env.ELECTRON_APP_NAME) {
18-
const appName = process.env.ELECTRON_APP_NAME;
19-
const userDataPath = path.join(app.getPath("appData"), appName);
17+
if (isDev && process.env.ELECTRON_USER_DATA_PATH) {
18+
console.debug("`ELECTRON_USER_DATA_PATH` found, modifying `userData` path: \n"
19+
+ `\t${app.getPath("userData")} -> ${process.env.ELECTRON_USER_DATA_PATH}`);
2020

21-
console.log("`ELECTRON_APP_NAME` found, overriding `appName` and `userData` path: \n"
22-
+ `\t${app.getName()} -> ${appName}\n`
23-
+ `\t${app.getPath("userData")} -> ${userDataPath}`);
24-
25-
app.setName(appName);
26-
app.setPath("userData", userDataPath);
21+
app.setPath('userData', process.env.ELECTRON_USER_DATA_PATH);
2722
}
2823

2924
const menuTemplate = require('./app/menu-template');

playwright.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { defineConfig, devices } from '@playwright/test';
22

3-
const reporter: string[][string] = [['list'], ['html']];
3+
const reporter: any[] = [['list'], ['html']];
44

55
if (process.env.CI) {
6-
reporter.push(["github"]);
6+
reporter.push(['github']);
77
}
88

9-
109
export default defineConfig({
1110
testDir: './e2e-tests',
1211
fullyParallel: false,
1312
forbidOnly: !!process.env.CI,
1413
retries: process.env.CI ? 1 : 0,
1514
workers: process.env.CI ? undefined : 1,
1615
reporter,
16+
1717
use: {
1818
trace: 'on-first-retry'
1919
},

playwright/electron.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ exports.startApp = async () => {
77
const app = await electron.launch({ args: [electronAppPath] });
88
const context = await app.context();
99

10-
app.process().stdout.on('data', (data) => console.log(data.toString()));
11-
app.process().stderr.on('data', (error) => console.error(error.toString()));
10+
app.process().stdout.on('data', (data) => {
11+
process.stdout.write(data.toString().replace(/^(?=.)/gm, '[Electron] |'));
12+
});
13+
app.process().stderr.on('data', (error) => {
14+
process.stderr.write(error.toString().replace(/^(?=.)/gm, '[Electron] |'));
15+
});
1216
return { app, context };
1317
};

playwright/index.ts

Lines changed: 167 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,178 @@
1-
import { test as baseTest, ElectronApplication, Page } from '@playwright/test';
1+
import { test as baseTest, BrowserContext, ElectronApplication, Page } from '@playwright/test';
2+
import * as path from 'path';
3+
import * as os from 'os';
4+
import * as fs from 'fs';
25

3-
const { startApp } = require('./electron.ts');
6+
const electronAppPath = path.join(__dirname, '../packages/bruno-electron');
47

5-
export const test = baseTest.extend<{ page: Page }, { electronApp: ElectronApplication }>({
6-
electronApp: [
8+
export const test = baseTest.extend<
9+
{
10+
context: BrowserContext;
11+
page: Page;
12+
newPage: Page;
13+
pageWithUserData: Page;
14+
},
15+
{
16+
createTmpDir: (tag?: string) => Promise<string>;
17+
launchElectronApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>;
18+
electronApp: ElectronApplication;
19+
reuseOrLaunchElectronApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>;
20+
}
21+
>({
22+
createTmpDir: [
723
async ({}, use) => {
8-
const { app: electronApp, context } = await startApp();
24+
const dirs: string[] = [];
25+
await use(async (tag?: string) => {
26+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `pw-${tag || ''}-`));
27+
dirs.push(dir);
28+
return dir;
29+
});
30+
await Promise.all(
31+
dirs.map((dir) => fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch((e) => e))
32+
);
33+
},
34+
{ scope: 'worker' }
35+
],
36+
37+
launchElectronApp: [
38+
async ({ playwright, createTmpDir }, use, workerInfo) => {
39+
const apps: ElectronApplication[] = [];
40+
await use(async ({ initUserDataPath } = {}) => {
41+
const userDataPath = await createTmpDir('electron-userdata');
42+
43+
if (initUserDataPath) {
44+
const replacements = {
45+
projectRoot: path.join(__dirname, '..')
46+
};
947

10-
await use(electronApp);
11-
await context.close();
12-
await electronApp.close();
48+
for (const file of await fs.promises.readdir(initUserDataPath)) {
49+
let content = await fs.promises.readFile(path.join(initUserDataPath, file), 'utf-8');
50+
content = content.replace(/{{(\w+)}}/g, (_, key) => {
51+
if (replacements[key]) {
52+
return replacements[key];
53+
} else {
54+
throw new Error(`\tNo replacement for {{${key}}} in ${path.join(initUserDataPath, file)}`);
55+
}
56+
});
57+
await fs.promises.writeFile(path.join(userDataPath, file), content, 'utf-8');
58+
}
59+
}
60+
61+
const app = await playwright._electron.launch({
62+
args: [electronAppPath],
63+
env: {
64+
ELECTRON_USER_DATA_PATH: userDataPath
65+
}
66+
});
67+
68+
const { workerIndex } = workerInfo;
69+
app.process().stdout.on('data', (data) => {
70+
process.stdout.write(data.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`));
71+
});
72+
app.process().stderr.on('data', (error) => {
73+
process.stderr.write(error.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`));
74+
});
75+
76+
apps.push(app);
77+
return app;
78+
});
79+
for (const app of apps) {
80+
await app.context().close();
81+
await app.close();
82+
}
1383
},
1484
{ scope: 'worker' }
1585
],
16-
page: async ({ electronApp }, use) => {
86+
87+
electronApp: [
88+
async ({ launchElectronApp }, use) => {
89+
const app = await launchElectronApp();
90+
await use(app);
91+
},
92+
{ scope: 'worker' }
93+
],
94+
95+
context: async ({ electronApp }, use, testInfo) => {
96+
const context = await electronApp.context();
97+
const tracingOptions = (testInfo as any)._tracing.traceOptions();
98+
if (tracingOptions) {
99+
try {
100+
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
101+
} catch (e) {}
102+
}
103+
await use(context);
104+
},
105+
106+
page: async ({ electronApp, context }, use, testInfo) => {
17107
const page = await electronApp.firstWindow();
18-
await use(page);
19-
await page.reload();
108+
const tracingOptions = (testInfo as any)._tracing.traceOptions();
109+
if (tracingOptions) {
110+
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
111+
await context.tracing.startChunk();
112+
await use(page);
113+
await context.tracing.stopChunk({ path: tracePath });
114+
await testInfo.attach('trace', { path: tracePath });
115+
} else {
116+
await use(page);
117+
}
118+
},
119+
120+
newPage: async ({ launchElectronApp }, use, testInfo) => {
121+
const app = await launchElectronApp();
122+
const context = await app.context();
123+
const page = await app.firstWindow();
124+
const tracingOptions = (testInfo as any)._tracing.traceOptions();
125+
if (tracingOptions) {
126+
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
127+
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
128+
await use(page);
129+
await context.tracing.stop({ path: tracePath });
130+
await testInfo.attach('trace', { path: tracePath });
131+
} else {
132+
await use(page);
133+
}
134+
},
135+
136+
reuseOrLaunchElectronApp: [
137+
async ({ launchElectronApp }, use, testInfo) => {
138+
const apps: Record<string, ElectronApplication> = {};
139+
await use(async ({ initUserDataPath } = {}) => {
140+
const key = initUserDataPath;
141+
if (key && apps[key]) {
142+
return apps[key];
143+
}
144+
const app = await launchElectronApp({ initUserDataPath });
145+
apps[key] = app;
146+
return app;
147+
});
148+
},
149+
{ scope: 'worker' }
150+
],
151+
152+
pageWithUserData: async ({ reuseOrLaunchElectronApp }, use, testInfo) => {
153+
const testDir = path.dirname(testInfo.file);
154+
const initUserDataPath = path.join(testDir, 'init-user-data');
155+
156+
const app = await reuseOrLaunchElectronApp(
157+
(await fs.promises.stat(initUserDataPath).catch(() => false)) ? { initUserDataPath } : {}
158+
);
159+
160+
const context = await app.context();
161+
const page = await app.firstWindow();
162+
const tracingOptions = (testInfo as any)._tracing.traceOptions();
163+
if (tracingOptions) {
164+
const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);
165+
try {
166+
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
167+
} catch (e) {}
168+
await context.tracing.startChunk();
169+
await use(page);
170+
await context.tracing.stopChunk({ path: tracePath });
171+
await testInfo.attach('trace', { path: tracePath });
172+
} else {
173+
await use(page);
174+
}
20175
}
21176
});
22177

23-
export * from '@playwright/test'
178+
export * from '@playwright/test';

0 commit comments

Comments
 (0)