Skip to content

Commit 7c9346d

Browse files
committed
chore: add e2e tests for custom linting rules
1 parent bece185 commit 7c9346d

10 files changed

Lines changed: 776 additions & 96 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
extends: [spectral:oas]
2+
rules:
3+
require-x-smoke-test-marker:
4+
description: info object must contain a custom x-smoke-test-marker field
5+
message: "{{description}}"
6+
severity: error
7+
given: $.info
8+
then:
9+
field: x-smoke-test-marker
10+
function: truthy
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
extends: [spectral:oas]
2+
# "functions" is a disallowed top-level key (RCE vector); upload should be rejected.
3+
functions:
4+
- my-custom-function
5+
rules:
6+
require-x-smoke-test-marker:
7+
description: info object must contain a custom x-smoke-test-marker field
8+
message: "{{description}}"
9+
severity: error
10+
given: $.info
11+
then:
12+
field: x-smoke-test-marker
13+
function: truthy
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { ElectronApplication, PlaywrightWorkerArgs } from '@playwright/test';
2+
3+
import { bundleType, cwd, executablePath, mainPath } from './paths';
4+
5+
export interface EnvOptions {
6+
INSOMNIA_DATA_PATH: string;
7+
INSOMNIA_API_URL: string;
8+
INSOMNIA_APP_WEBSITE_URL: string;
9+
INSOMNIA_AI_URL: string;
10+
INSOMNIA_MOCK_API_URL: string;
11+
INSOMNIA_GITHUB_REST_API_URL: string;
12+
INSOMNIA_GITHUB_API_URL: string;
13+
INSOMNIA_GITLAB_API_URL: string;
14+
INSOMNIA_UPDATES_URL: string;
15+
INSOMNIA_SKIP_ONBOARDING: string;
16+
INSOMNIA_PUBLIC_KEY: string;
17+
INSOMNIA_SECRET_KEY: string;
18+
INSOMNIA_SESSION?: string;
19+
INSOMNIA_VAULT_KEY: string;
20+
INSOMNIA_VAULT_SALT: string;
21+
INSOMNIA_VAULT_SRP_SECRET: string;
22+
KONNECT_API_URL: string;
23+
}
24+
25+
/**
26+
* Tracks every ElectronApplication launched during a test so the `app` fixture
27+
* teardown can close any that survive (e.g. instances created by relaunch()).
28+
*/
29+
export const liveApps = new Set<ElectronApplication>();
30+
31+
/**
32+
* Launches Insomnia with the given env options. Extracted from the `app` fixture
33+
* so tests can perform a real process-level relaunch (see InsomniaApp.relaunch).
34+
*/
35+
export async function launchInsomnia(
36+
playwright: PlaywrightWorkerArgs['playwright'],
37+
envOptions: EnvOptions,
38+
): Promise<ElectronApplication> {
39+
const { ELECTRON_RUN_AS_NODE: _ignored, ...launchEnv } = process.env;
40+
const app = await playwright._electron.launch({
41+
cwd,
42+
executablePath,
43+
args: bundleType() === 'package' ? ['--no-sandbox'] : ['--no-sandbox', mainPath],
44+
env: {
45+
...launchEnv,
46+
...envOptions,
47+
PLAYWRIGHT: 'true',
48+
},
49+
});
50+
liveApps.add(app);
51+
app.on('close', () => liveApps.delete(app));
52+
return app;
53+
}

packages/insomnia-smoke-test/playwright/pages/insomnia-app.ts

Lines changed: 126 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
import type { ElectronApplication, Page } from '@playwright/test';
22

3+
import { launchInsomnia } from '../launch';
34
import { ExportModal } from './components/export-modal';
45
import { NavigationSidebar } from './components/navigation-sidebar';
56
import { StatusbarComponent } from './components/statusbar';
67
import { PreferencesPage } from './preferences';
78
import { ProjectPage } from './project';
89
import { WorkspacePage } from './workspace';
910

11+
/**
12+
* `ElectronApplication` with the launch-env metadata stashed by the `app`
13+
* fixture. Named here to avoid the duplicated intersection cast that used to
14+
* appear inside `relaunch()` and `launchClone()`.
15+
*/
16+
type StashedApp = ElectronApplication & {
17+
__launchEnv?: Record<string, string>;
18+
__playwright?: any;
19+
};
20+
21+
/** Attach launch-env metadata to an `ElectronApplication` instance. */
22+
function stashLaunchEnv(app: ElectronApplication, env: Record<string, string>, playwright: any) {
23+
const s = app as StashedApp;
24+
s.__launchEnv = env;
25+
s.__playwright = playwright;
26+
}
27+
1028
/**
1129
* Root facade for the Insomnia E2E Page Object Model.
1230
*
@@ -42,40 +60,67 @@ export class InsomniaApp {
4260
// ===========================================================================
4361

4462
/** Statusbar (footer) — always visible. */
45-
readonly statusbar: StatusbarComponent;
63+
statusbar!: StatusbarComponent;
4664

4765
// global export modal
48-
readonly exportModal: ExportModal;
66+
exportModal!: ExportModal;
4967

5068
/** Project navigation sidebar — always visible (except login). */
51-
readonly navigationSidebar: NavigationSidebar;
69+
navigationSidebar!: NavigationSidebar;
5270

5371
// ===========================================================================
5472
// Page objects
5573
// ===========================================================================
5674

5775
/** Project page (project/file list). */
58-
readonly projectPage: ProjectPage;
76+
projectPage!: ProjectPage;
5977

6078
/** Workspace page (debug view). */
61-
readonly workspacePage: WorkspacePage;
79+
workspacePage!: WorkspacePage;
6280

6381
/** Preferences page (settings modal). */
64-
readonly preferencesPage: PreferencesPage;
65-
66-
constructor(
67-
readonly page: Page,
68-
readonly app: ElectronApplication,
69-
) {
70-
// Shared components
71-
this.statusbar = new StatusbarComponent(page);
72-
this.exportModal = new ExportModal(page);
73-
this.navigationSidebar = new NavigationSidebar(page);
74-
75-
// Pages
76-
this.projectPage = new ProjectPage(page, app);
77-
this.workspacePage = new WorkspacePage(page, app);
78-
this.preferencesPage = new PreferencesPage(page, app);
82+
preferencesPage!: PreferencesPage;
83+
84+
// Private backing fields exposed as readonly getters so that external callers
85+
// cannot reassign them, while `relaunch()` can update them after a relaunch.
86+
private _page: Page;
87+
private _app: ElectronApplication;
88+
89+
get page(): Page {
90+
return this._page;
91+
}
92+
93+
get app(): ElectronApplication {
94+
return this._app;
95+
}
96+
97+
constructor(page: Page, app: ElectronApplication) {
98+
this._page = page;
99+
this._app = app;
100+
this._initPageObjects();
101+
}
102+
103+
private _initPageObjects() {
104+
this.statusbar = new StatusbarComponent(this._page);
105+
this.exportModal = new ExportModal(this._page);
106+
this.navigationSidebar = new NavigationSidebar(this._page);
107+
this.projectPage = new ProjectPage(this._page, this._app);
108+
this.workspacePage = new WorkspacePage(this._page, this._app);
109+
this.preferencesPage = new PreferencesPage(this._page, this._app);
110+
}
111+
112+
/** Read the stashed launch env/playwright from the underlying Electron app, or throw. */
113+
private _unstash(): { env: Record<string, string>; playwright: any } {
114+
const s = this._app as StashedApp;
115+
const env = s.__launchEnv;
116+
const playwright = s.__playwright;
117+
if (!env || !playwright) {
118+
throw new Error(
119+
'Launch env was not stashed on the ElectronApplication. ' +
120+
'Ensure the test was started via the `app` fixture in playwright/test.ts.',
121+
);
122+
}
123+
return { env, playwright };
79124
}
80125

81126
// ===========================================================================
@@ -84,6 +129,66 @@ export class InsomniaApp {
84129

85130
/** Press Escape on the app container (closes modals, dropdowns, overlays). */
86131
async pressEscape(): Promise<void> {
87-
await this.page.locator('.app').press('Escape');
132+
await this._page.locator('.app').press('Escape');
133+
}
134+
135+
/**
136+
* Queue a fake response for the next Electron `showOpenDialog` call.
137+
* Consumed by the main-process handler when `PLAYWRIGHT === 'true'`.
138+
*/
139+
async queueOpenDialogResponse(filePaths: string[], canceled = false): Promise<void> {
140+
await this._app.evaluate(
141+
(_electron, payload) => {
142+
const g = globalThis as any;
143+
g.__PLAYWRIGHT_OPEN_DIALOG_QUEUE__ ||= [];
144+
g.__PLAYWRIGHT_OPEN_DIALOG_QUEUE__.push(payload);
145+
},
146+
{ filePaths, canceled },
147+
);
148+
}
149+
150+
/**
151+
* Close the current Electron process and relaunch it reusing the same env
152+
* vars (including INSOMNIA_DATA_PATH) so on-disk state — NeDB, secret store —
153+
* is preserved across the cycle. After this returns, `this.app` and
154+
* `this.page` point at the fresh process; all page objects are rebuilt.
155+
*
156+
* The launch env is stashed on the app instance by the `app` fixture in
157+
* `playwright/test.ts`; callers don't need to pass anything.
158+
*/
159+
async relaunch(): Promise<void> {
160+
const { env, playwright } = this._unstash();
161+
await this._app.close();
162+
163+
const next = await launchInsomnia(playwright, env as any);
164+
stashLaunchEnv(next, env, playwright);
165+
166+
this._app = next;
167+
this._page = await next.firstWindow({ timeout: 60_000 });
168+
await this._page.waitForLoadState();
169+
// Re-seed the konnect PAT like the page fixture does.
170+
await this._page.evaluate(() => (window as any).main.secretStorage.setSecret('konnectPat', 'kpat_test'));
171+
172+
this._initPageObjects();
173+
}
174+
175+
/**
176+
* Launch a second Electron instance with a fresh data path and optional env
177+
* overrides (e.g. a different INSOMNIA_SESSION for a different user). The
178+
* returned InsomniaApp is independent — it has its own page and app references
179+
* and will be cleaned up by the `app` fixture's liveApps teardown.
180+
*/
181+
async launchClone(newDataPath: string, envOverrides: Record<string, string> = {}): Promise<InsomniaApp> {
182+
const { env, playwright } = this._unstash();
183+
const cloneEnv = { ...env, INSOMNIA_DATA_PATH: newDataPath, ...envOverrides };
184+
185+
const next = await launchInsomnia(playwright, cloneEnv as any);
186+
stashLaunchEnv(next, cloneEnv, playwright);
187+
188+
const page = await next.firstWindow({ timeout: 60_000 });
189+
await page.waitForLoadState();
190+
await page.evaluate(() => (window as any).main.secretStorage.setSecret('konnectPat', 'kpat_test'));
191+
192+
return new InsomniaApp(page, next);
88193
}
89194
}

packages/insomnia-smoke-test/playwright/test.ts

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// Read more about creating fixtures https://playwright.dev/docs/test-fixtures
33
import path from 'node:path';
44

5-
import type { ElectronApplication, TraceMode } from '@playwright/test';
5+
import type { ElectronApplication, PlaywrightWorkerArgs, TraceMode } from '@playwright/test';
66
import { test as baseTest } from '@playwright/test';
77

8+
import type { EnvOptions } from './launch';
9+
import { launchInsomnia, liveApps } from './launch';
810
import { InsomniaApp } from './pages';
9-
import { bundleType, cwd, executablePath, mainPath, randomDataPath } from './paths';
11+
import { randomDataPath } from './paths';
1012

1113
// Throw an error if the condition fails
1214
// > Not providing an inline default argument for message as the result is smaller
@@ -24,26 +26,6 @@ export function invariant(
2426
throw new Error(typeof message === 'function' ? message() : message);
2527
}
2628

27-
interface EnvOptions {
28-
INSOMNIA_DATA_PATH: string;
29-
INSOMNIA_API_URL: string;
30-
INSOMNIA_APP_WEBSITE_URL: string;
31-
INSOMNIA_AI_URL: string;
32-
INSOMNIA_MOCK_API_URL: string;
33-
INSOMNIA_GITHUB_REST_API_URL: string;
34-
INSOMNIA_GITHUB_API_URL: string;
35-
INSOMNIA_GITLAB_API_URL: string;
36-
INSOMNIA_UPDATES_URL: string;
37-
INSOMNIA_SKIP_ONBOARDING: string;
38-
INSOMNIA_PUBLIC_KEY: string;
39-
INSOMNIA_SECRET_KEY: string;
40-
INSOMNIA_SESSION?: string;
41-
INSOMNIA_VAULT_KEY: string;
42-
INSOMNIA_VAULT_SALT: string;
43-
INSOMNIA_VAULT_SRP_SECRET: string;
44-
KONNECT_API_URL: string;
45-
}
46-
4729
interface AESMessage {
4830
iv: string;
4931
t: string;
@@ -99,18 +81,16 @@ export const test = baseTest.extend<{
9981
KONNECT_API_URL: echoServer,
10082
...(userConfig.session ? { INSOMNIA_SESSION: JSON.stringify(userConfig.session) } : {}),
10183
};
102-
const { ELECTRON_RUN_AS_NODE: _ignored, ...launchEnv } = process.env;
103-
104-
const electronApp = await playwright._electron.launch({
105-
cwd,
106-
executablePath,
107-
args: bundleType() === 'package' ? ['--no-sandbox'] : ['--no-sandbox', mainPath],
108-
env: {
109-
...launchEnv,
110-
...options,
111-
PLAYWRIGHT: 'true',
112-
},
113-
});
84+
85+
const electronApp = await launchInsomnia(playwright, options);
86+
// Stash the launch options on the app so InsomniaApp.relaunch() can reuse them
87+
// without re-deriving env from fixtures.
88+
const stashed = electronApp as ElectronApplication & {
89+
__launchEnv?: EnvOptions;
90+
__playwright?: PlaywrightWorkerArgs['playwright'];
91+
};
92+
stashed.__launchEnv = options;
93+
stashed.__playwright = playwright;
11494

11595
const appContext = electronApp.context();
11696

@@ -145,14 +125,29 @@ export const test = baseTest.extend<{
145125
// Use a different name rather than the default trace.zip to avoid overwriting the trace.
146126
// Refer: https://github.com/microsoft/playwright/issues/35005
147127
// Discard the trace if not needed
148-
await (isTrace
149-
? appContext.tracing.stop({
150-
path: path.join(testInfo.outputDir, `trace-${testInfo.title}-${testInfo.status}.zip`),
151-
})
152-
: appContext.tracing.stop());
128+
// The app may have been relaunched during the test (e.g. insomnia.relaunch()), which closes
129+
// the original Electron process and invalidates appContext. Guard against that here.
130+
try {
131+
await (isTrace
132+
? appContext.tracing.stop({
133+
path: path.join(testInfo.outputDir, `trace-${testInfo.title}-${testInfo.status}.zip`),
134+
})
135+
: appContext.tracing.stop());
136+
} catch {
137+
// Original app was closed by relaunch(); tracing on the new app is not captured.
138+
}
153139
}
154140

155-
await electronApp.close();
141+
// Close any apps that are still alive (e.g. relaunched copies). Snapshot
142+
// first: close() fires the 'close' listener which calls liveApps.delete(),
143+
// so iterating the live Set would skip un-visited entries.
144+
for (const live of Array.from(liveApps)) {
145+
try {
146+
await live.close();
147+
} catch {
148+
// Best-effort: an already-closed app rejects; ignore.
149+
}
150+
}
156151
},
157152
page: async ({ app }, use) => {
158153
// The plugin window is created after the main window's did-finish-load, so

0 commit comments

Comments
 (0)