Skip to content

Commit 61721a1

Browse files
committed
En-doc-inate
1 parent 7946657 commit 61721a1

8 files changed

Lines changed: 111 additions & 14 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
run: npm ci --ignore-scripts --omit=optional
8181

8282
- name: Install system dependencies for Electron
83-
run: sudo apt-get update && sudo apt-get install -y xvfb libgbm-dev libnss3 libxss1 libasound2t64
83+
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb libgbm-dev libnss3 libxss1 libasound2t64
8484

8585
- name: Build extension
8686
working-directory: extension-repo

e2e-tests/fixtures/app.fixture.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export interface TestAppFixtures {
2525
mainPage: Page;
2626
}
2727

28+
/**
29+
* Playwright test fixture for smoke tests. Launches one Electron instance per worker (shared across
30+
* all tests in that worker) and provides `electronApp` and `mainPage`. Attaches a failure
31+
* screenshot to the report when a test does not meet its expected status.
32+
*/
2833
export const test = base.extend<TestAppFixtures, WorkerAppFixtures>({
2934
// Worker-scoped: the Electron process is launched once per worker and shared across all tests,
3035
// avoiding the process startup/teardown cost per test.

e2e-tests/fixtures/cdp.fixture.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@ export { expect } from '@playwright/test';
1818

1919
const CDP_URL = process.env.CDP_URL || 'http://localhost:9223';
2020

21+
/** Fixtures provided by the CDP test fixture. */
2122
export interface CdpFixtures {
2223
mainPage: Page;
2324
}
2425

26+
/**
27+
* Playwright test fixture for feature tests. Connects to an already-running Platform.Bible instance
28+
* via CDP and provides `mainPage`. Does not launch or shut down the app.
29+
*/
2530
export const test = base.extend<CdpFixtures>({
2631
// eslint-disable-next-line no-empty-pattern
2732
mainPage: async ({}, use) => {

e2e-tests/fixtures/helpers.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ export interface LaunchElectronAppOptions {
4343
envOverrides?: Record<string, string>;
4444
}
4545

46-
/** Wait for the WebSocket server to be ready on the specified port. */
46+
/**
47+
* Wait for the WebSocket server to be ready on the specified port.
48+
*
49+
* @param port Port number to connect to.
50+
* @param timeout Maximum time in milliseconds to wait before throwing.
51+
* @returns Resolves when a WebSocket connection to the port succeeds.
52+
* @throws {Error} If the WebSocket server is not ready within `timeout` milliseconds.
53+
*/
4754
async function waitForWebSocketReady(port: number, timeout: number): Promise<void> {
4855
const startTime = Date.now();
4956

@@ -79,8 +86,12 @@ async function waitForWebSocketReady(port: number, timeout: number): Promise<voi
7986

8087
/**
8188
* Launch a fresh Electron instance (paranext-core) with the interlinearizer extension loaded via
82-
* `--extensionDirs`. Returns the app handle, the temp directory path, and a promise that resolves
83-
* when the app closes.
89+
* `--extensions`.
90+
*
91+
* @param opts Optional launch options (e.g. environment variable overrides).
92+
* @returns The app handle, the isolated user-data directory path, and a promise that resolves when
93+
* the app closes.
94+
* @throws If Electron fails to launch or the WebSocket server does not become ready.
8495
*/
8596
export async function launchElectronWithExtension(
8697
opts: LaunchElectronAppOptions = {},
@@ -161,6 +172,9 @@ export async function launchElectronWithExtension(
161172
/**
162173
* Tear down an Electron instance: kill the process group, wait for close, and clean up the isolated
163174
* user-data directory.
175+
*
176+
* @param ctx The app context returned by {@link launchElectronWithExtension}.
177+
* @returns Resolves when the Electron process has been killed and user-data cleaned up.
164178
*/
165179
export async function teardownElectronApp(ctx: ElectronAppContext): Promise<void> {
166180
const { electronApp, userDataDir, appClosed } = ctx;
@@ -218,6 +232,15 @@ export async function teardownElectronApp(ctx: ElectronAppContext): Promise<void
218232
/**
219233
* One JSON-RPC 2.0 request over WebSocket: open, send, wait for response id `1`, close. Ignores
220234
* unrelated messages until the matching response arrives.
235+
*
236+
* @param method JSON-RPC method name to invoke.
237+
* @param timeoutErrorMessage Custom error message on timeout; defaults to a standard timeout
238+
* message.
239+
* @param params Positional parameters to send with the request.
240+
* @param port WebSocket port to connect to.
241+
* @param perRequestTimeoutMs Milliseconds before the request times out.
242+
* @returns The `result` field of the JSON-RPC response, typed as `T`.
243+
* @throws {Error} If the request times out or the server returns a JSON-RPC error.
221244
*/
222245
async function sendPapiJsonRpcOnce<T>(
223246
method: string,
@@ -278,6 +301,13 @@ async function sendPapiJsonRpcOnce<T>(
278301
/**
279302
* Send a single JSON-RPC request where `method` is a PAPI request type (e.g. `rpc.discover`). Opens
280303
* a connection, sends one request, waits for the matching response id, then closes.
304+
*
305+
* @param method PAPI request type to invoke (e.g. `rpc.discover`).
306+
* @param params Positional parameters to send with the request.
307+
* @param port WebSocket port to connect to.
308+
* @param perRequestTimeoutMs Milliseconds before the request times out.
309+
* @returns The `result` field of the JSON-RPC response, typed as `T`.
310+
* @throws {Error} If the request times out or the server returns a JSON-RPC error.
281311
*/
282312
export async function sendPapiRequestOnce<T>(
283313
method: string,
@@ -288,7 +318,15 @@ export async function sendPapiRequestOnce<T>(
288318
return sendPapiJsonRpcOnce<T>(method, undefined, params, port, perRequestTimeoutMs);
289319
}
290320

291-
/** Poll `rpc.discover` until `methodName` appears in `result.methods` or `timeoutMs` elapses. */
321+
/**
322+
* Poll `rpc.discover` until `methodName` appears in `result.methods` or `timeoutMs` elapses.
323+
*
324+
* @param methodName The fully-qualified PAPI method name to wait for (e.g. `command:foo.bar`).
325+
* @param port WebSocket port to connect to.
326+
* @param timeoutMs Maximum time in milliseconds to poll before throwing.
327+
* @returns Resolves when the method appears in `rpc.discover`.
328+
* @throws {Error} If the method is not registered within `timeoutMs` milliseconds.
329+
*/
292330
export async function waitForPapiMethodRegistered(
293331
methodName: string,
294332
port: number = DEFAULT_WEBSOCKET_PORT,
@@ -320,6 +358,12 @@ export async function waitForPapiMethodRegistered(
320358
/**
321359
* Wait for the Platform.Bible UI to be fully ready: dock layout appears and `platform.about`
322360
* command is registered (dialog service has finished initializing).
361+
*
362+
* @param page The Playwright `Page` for the Platform.Bible renderer window.
363+
* @param timeout Maximum time in milliseconds to wait before throwing.
364+
* @returns Resolves when the dock layout is visible and `platform.about` is registered.
365+
* @throws If the dock layout or `platform.about` command does not appear within `timeout`
366+
* milliseconds.
323367
*/
324368
export async function waitForAppReady(page: Page, timeout = 60_000): Promise<void> {
325369
const start = Date.now();
@@ -334,6 +378,10 @@ export async function waitForAppReady(page: Page, timeout = 60_000): Promise<voi
334378
/**
335379
* Wait for the interlinearizer extension to finish activating by polling `rpc.discover` until
336380
* `interlinearizer.openForWebView` is listed.
381+
*
382+
* @param timeoutMs Maximum time in milliseconds to poll before throwing.
383+
* @returns Resolves when `interlinearizer.openForWebView` is listed in `rpc.discover`.
384+
* @throws {Error} If the extension does not register within `timeoutMs` milliseconds.
337385
*/
338386
export async function waitForInterlinearizerReady(timeoutMs = 90_000): Promise<void> {
339387
await waitForPapiMethodRegistered(

e2e-tests/global-setup.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import fs from 'fs';
88
const WEBSOCKET_PORT = 8876;
99
const RENDERER_PORT = 1212;
1010

11-
/** Check if a port is already in use */
11+
/**
12+
* Check if a port is already in use.
13+
*
14+
* @param port Port number to probe.
15+
* @returns Resolves to `true` if the port is occupied, `false` if it is free.
16+
*/
1217
function isPortInUse(port: number): Promise<boolean> {
1318
return new Promise((resolve) => {
1419
const server = net.createServer();
@@ -24,7 +29,14 @@ function isPortInUse(port: number): Promise<boolean> {
2429
});
2530
}
2631

27-
/** Wait until a port is accepting connections */
32+
/**
33+
* Wait until a port is accepting connections.
34+
*
35+
* @param port Port number to poll.
36+
* @param timeout Maximum time in milliseconds to wait before rejecting.
37+
* @returns Resolves when a TCP connection to the port succeeds.
38+
* @throws {Error} If the port does not become available within `timeout` milliseconds.
39+
*/
2840
function waitForPort(port: number, timeout: number): Promise<void> {
2941
const startTime = Date.now();
3042
return new Promise((resolve, reject) => {
@@ -47,7 +59,23 @@ function waitForPort(port: number, timeout: number): Promise<void> {
4759
});
4860
}
4961

50-
// Playwright global setup requires this signature even though config is unused
62+
/**
63+
* Playwright global setup. Runs once before any test worker starts.
64+
*
65+
* 1. Fails fast if port 8876 is already in use (a running Platform.Bible would conflict with the
66+
* Electron instance launched by fixtures).
67+
* 2. Removes stale Electron singleton lock files left behind by crashes.
68+
* 3. Fails fast if the extension dist is missing (directs the developer to run `npm run build`).
69+
* 4. Ensures the paranext-core dev main bundle exists, building it via `npm run prestart` if not.
70+
* 5. Starts the paranext-core webpack renderer dev server on port 1212 if not already running, and
71+
* stores its PID for {@link globalTeardown} to stop it.
72+
*
73+
* @param _config Playwright config object — unused; required by Playwright's global-setup
74+
* interface.
75+
* @returns Resolves when the renderer dev server is ready.
76+
* @throws {Error} If port 8876 is already in use.
77+
* @throws {Error} If the extension dist is missing.
78+
*/
5179
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5280
export default async function globalSetup(_config: FullConfig): Promise<void> {
5381
const extensionRoot = path.resolve(__dirname, '..');

e2e-tests/global-teardown.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ import { execSync } from 'child_process';
44
import path from 'path';
55
import fs from 'fs';
66

7-
// Playwright global teardown requires this signature even though config is unused
7+
/**
8+
* Playwright global teardown. Runs once after all test workers have finished.
9+
*
10+
* Stops the renderer dev server started by {@link globalSetup} (if any), then runs `npm run stop` in
11+
* paranext-core to terminate any lingering Electron processes.
12+
*
13+
* @param _config Playwright config object — unused; required by Playwright's global-teardown
14+
* interface.
15+
* @returns Resolves when all cleanup steps have completed.
16+
*/
817
// eslint-disable-next-line @typescript-eslint/no-unused-vars
918
export default async function globalTeardown(_config: FullConfig): Promise<void> {
1019
const extensionRoot = path.resolve(__dirname, '..');

e2e-tests/playwright.config.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { defineConfig } from '@playwright/test';
44
/**
55
* Playwright configuration for interlinearizer extension E2E tests.
66
*
7-
* Launches Platform.Bible with the interlinearizer extension loaded via `--extensionDirs`.
7+
* Launches Platform.Bible with the interlinearizer extension loaded via `--extensions`.
88
*
99
* Prerequisites:
1010
*
@@ -38,9 +38,5 @@ export default defineConfig({
3838
name: 'smoke',
3939
testDir: './tests/smoke',
4040
},
41-
{
42-
name: 'isolated',
43-
testDir: './tests/isolated',
44-
},
4541
],
4642
});

e2e-tests/tests/_example/example-interlinearizer-feature.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
import { test, expect } from '../../fixtures/cdp.fixture';
1717
import { waitForAppReady, waitForInterlinearizerReady } from '../../fixtures/helpers';
1818

19+
/**
20+
* Filter out expected/benign console errors from a list of captured error messages.
21+
*
22+
* @param errors Array of console error message strings to filter.
23+
* @returns The subset of `errors` that are not considered benign.
24+
*/
1925
function filterConsoleErrors(errors: string[]): string[] {
2026
return errors.filter(
2127
(e) =>

0 commit comments

Comments
 (0)