Phase 1 step 7 of VNCprodbuild. integration/tests/11-electron-notification.spec.ts launches the actual Electron shell, logs in as alice against this repo's existing docker-compose Stalwart fixture, injects a message over real SMTP (same helpers/smtp.ts sendMail() 02-mail-sync.spec.ts uses), and asserts a native notification fires via electron/main.ts's __notificationCallCount test hook - proving the full real pipeline, not just the synthetic IPC call step 3's smoke test exercises: SMTP -> Stalwart -> JMAP push (lib/jmap/client.ts) -> stores/email-store.ts's handleStateChange -> handleNewEmailNotification -> the page effect -> lib/electron-bridge.ts -> the contextBridge/IPC bridge -> electron/main.ts's Notification call. Runs against a `next dev` server (electron/main.ts's new ELECTRON_LOAD_URL escape hatch), not the standalone build, because this fixture's Stalwart is deliberately plain HTTP and production's CSP correctly refuses non-TLS connections - the identical trade-off integration/webmail.Dockerfile already makes for the browser-based suite. New playwright.integration-electron.config.ts + global-setup-electron.ts (brings up only the `stalwart` compose service, not `webmail`, which this suite never touches and which may not even be startable on a given host - see its own header comment) keep this fully separate from the main dockerized integration run, which has no Electron binary compatible with that container's platform; playwright.integration.config.ts gets a matching testIgnore so a plain `npm run test:integration` never tries to sweep this file in. Wired as `npm run test:integration:electron`. On "the real WebSocket path": confirmed against this fixture's actual `stalwartlabs/stalwart:v0.16` (same as the sandbox server) that its /jmap/ws requires the same Authorization header as every other JMAP endpoint on the handshake itself, which the browser WebSocket API cannot attach - so the WS attempt reaches the network correctly (see the CSP fix in the previous commit) but always fails auth here, and the circuit breaker falls back to SSE within about a second. That fallback is what delivers the push this test observes - documented in detail in the spec's header comment, including why asserting the WS handshake itself succeeds here would be asserting something that cannot be true from a browser against this specific server. Known flakiness, root-caused not eliminated (see playwright.integration-electron.config.ts's retries: 2 and its comment): `next dev`'s on-demand route compilation + Fast Refresh occasionally races the SSE stream during the login -> inbox transition and drops that one push event with no error anywhere - reproduced by running the identical test repeatedly against an already-warm stack (IT_NO_DOCKER=1): identical request sequence logged every time, but the outcome wasn't always the same. This is specific to the dev-server workaround this test needs for the plaintext-Stalwart fixture, not a bug in the feature it's verifying - the WS circuit breaker and SSE fallback fire exactly as designed in every run's own logs, pass or fail. Verified: passed cleanly standalone multiple times; with retries: 2 in place, passed within the retry budget on every attempt made.
86 lines
3.8 KiB
TypeScript
86 lines
3.8 KiB
TypeScript
/**
|
|
* Global setup for playwright.integration-electron.config.ts - a narrower
|
|
* variant of ./global-setup.ts.
|
|
*
|
|
* The Electron suite (11-electron-notification.spec.ts) boots its OWN
|
|
* standalone Next.js server via electron/main.ts, so unlike the main
|
|
* integration config it never talks to the docker-compose `webmail`
|
|
* container on :3000 at all - only to `stalwart` (JMAP + SMTP). Bringing up
|
|
* `webmail` too would be pointless work, and on a host where something else
|
|
* already owns port 3000 (this repo doesn't own that port - any other
|
|
* project's dev server can be sitting on it) it would fail outright for a
|
|
* container this suite never uses. `docker compose up <service>` scopes the
|
|
* bring-up to just `stalwart`.
|
|
*
|
|
* Set IT_NO_DOCKER=1 to skip container management entirely (useful when the
|
|
* stack is already running).
|
|
*/
|
|
import { execFileSync } from 'node:child_process';
|
|
import { existsSync, copyFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { JMAP_URL, ACCOUNTS, ACCOUNT_PASSWORD } from './helpers/config';
|
|
|
|
const INTEGRATION_DIR = path.resolve(__dirname, '..');
|
|
const COMPOSE_FILE = path.join(INTEGRATION_DIR, 'docker-compose.yml');
|
|
const ENV_FILE = path.join(INTEGRATION_DIR, '.env');
|
|
const STALWART_CLI_BIN = path.join(INTEGRATION_DIR, 'stalwart', 'stalwart-cli');
|
|
|
|
function run(cmd: string, args: string[]): void {
|
|
execFileSync(cmd, args, { cwd: INTEGRATION_DIR, stdio: 'inherit' });
|
|
}
|
|
|
|
async function waitForStalwart(timeoutMs = 240000): Promise<void> {
|
|
const url = `${JMAP_URL}/jmap/session`;
|
|
const deadline = Date.now() + timeoutMs;
|
|
const auth = 'Basic ' + Buffer.from(`${ACCOUNTS.alice.email}:${ACCOUNT_PASSWORD}`).toString('base64');
|
|
for (;;) {
|
|
try {
|
|
const res = await fetch(url, { headers: { Authorization: auth } });
|
|
if (res.ok) return;
|
|
} catch {
|
|
/* not up yet */
|
|
}
|
|
if (Date.now() > deadline) throw new Error(`Timed out waiting for Stalwart JMAP at ${url}`);
|
|
await new Promise((r) => setTimeout(r, 2000));
|
|
}
|
|
}
|
|
|
|
export default async function globalSetup(): Promise<void> {
|
|
if (process.env.IT_NO_DOCKER === '1') {
|
|
console.log('[global-setup-electron] IT_NO_DOCKER=1 - skipping docker compose management');
|
|
} else {
|
|
// stalwart/prepare-stalwart-cli.sh fetches a LINUX binary (it's COPYed
|
|
// into the Stalwart container by integration/stalwart/Dockerfile - never
|
|
// meant to run on the host at all) but ends by executing it as its own
|
|
// sanity check, which only works when the host itself is Linux. On a
|
|
// macOS host that self-check fails outright ("cannot execute binary
|
|
// file") even though the download+extract already succeeded and the
|
|
// file the Dockerfile needs is perfectly fine on disk. Skipping the
|
|
// script once the binary already exists sidesteps that host/target
|
|
// mismatch without touching the shared script (used by the main
|
|
// integration config too, on hosts where it does work).
|
|
if (existsSync(STALWART_CLI_BIN)) {
|
|
console.log('[global-setup-electron] stalwart-cli already present, skipping fetch');
|
|
} else {
|
|
console.log('[global-setup-electron] fetching stalwart-cli');
|
|
run('bash', [path.join(INTEGRATION_DIR, 'stalwart', 'prepare-stalwart-cli.sh')]);
|
|
}
|
|
|
|
if (!existsSync(ENV_FILE)) {
|
|
console.log('[global-setup-electron] creating integration/.env from .env.example');
|
|
copyFileSync(path.join(INTEGRATION_DIR, '.env.example'), ENV_FILE);
|
|
}
|
|
|
|
console.log('[global-setup-electron] docker compose up -d --build --wait stalwart');
|
|
run('docker', [
|
|
'compose', '-f', COMPOSE_FILE, '--env-file', ENV_FILE,
|
|
'up', '-d', '--build', '--wait', '--wait-timeout', '300', 'stalwart',
|
|
]);
|
|
}
|
|
|
|
console.log('[global-setup-electron] waiting for Stalwart JMAP');
|
|
await waitForStalwart();
|
|
|
|
console.log('[global-setup-electron] stack ready');
|
|
}
|