Skip to content

Commit 20d9ad6

Browse files
committed
🧪 [test] Run published upgrades outside test workers
🧪 [test] Replace the Playwright runner case with a standalone typed smoke program that preserves download, handoff, screenshot, and log assertions. 🧪 [test] Exit explicitly after the detached updater takes control so Playwright worker teardown cannot hang on the self-replaced Electron process.
1 parent 08790eb commit 20d9ad6

2 files changed

Lines changed: 86 additions & 61 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@
265265
"test:tabs": "npm run build:runtime-ts && node --max-old-space-size=8192 ./node_modules/vitest/vitest.mjs --config vitest.config.ts --run --reporter=verbose tests/unit/tabs --maxWorkers 1",
266266
"test:ui": "npm run build:runtime-ts && node --max-old-space-size=8192 ./node_modules/vitest/vitest.mjs --config vitest.config.ts --ui",
267267
"test:unit": "npm run build:runtime-ts && node --max-old-space-size=8192 ./node_modules/vitest/vitest.mjs --config vitest.config.ts --run tests/unit --maxWorkers 1",
268-
"test:upgrade:published": "cross-env PW_DISABLE_TS_ESM=1 playwright test tests/playwright/published-upgrade.spec.ts --config playwright.config.ts",
268+
"test:upgrade:published": "node --experimental-strip-types tests/playwright/published-upgrade.ts",
269269
"test:watch": "npm run build:runtime-ts && node --max-old-space-size=8192 ./node_modules/vitest/vitest.mjs --config vitest.config.ts --watch",
270270
"test:watch:changed": "npm run build:runtime-ts && node --max-old-space-size=8192 ./node_modules/vitest/vitest.mjs --config vitest.config.ts --watch --changed",
271271
"typecheck": "tsc --project tsconfig.app.json --noEmit",

tests/playwright/published-upgrade.spec.ts renamed to tests/playwright/published-upgrade.ts

Lines changed: 85 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { expect, test, _electron as electron } from "@playwright/test";
2-
import type { ElectronApplication } from "@playwright/test";
1+
import assert from "node:assert/strict";
32
import fs from "node:fs";
43
import os from "node:os";
54
import path from "node:path";
5+
import { _electron as electron } from "@playwright/test";
6+
import type { ElectronApplication } from "@playwright/test";
67

78
type UpgradeConfiguration = {
89
evidencePath: string;
@@ -48,45 +49,64 @@ function getUpgradeConfiguration(): UpgradeConfiguration {
4849
};
4950
}
5051

51-
async function closeElectronApp(app: ElectronApplication): Promise<void> {
52-
const process = app.process();
53-
const exitPromise = new Promise<void>((resolve) =>
54-
process.once("exit", () => resolve())
55-
);
56-
const closePromise = app.close().catch(() => undefined);
52+
async function waitFor(
53+
predicate: () => boolean | Promise<boolean>,
54+
timeoutMs: number,
55+
failureMessage: string
56+
): Promise<void> {
57+
const deadline = Date.now() + timeoutMs;
58+
59+
while (Date.now() < deadline) {
60+
try {
61+
if (await predicate()) {
62+
return;
63+
}
64+
} catch {
65+
// The Electron connection can close while an update is handed off.
66+
}
67+
await new Promise((resolve) => setTimeout(resolve, 250));
68+
}
5769

58-
await Promise.race([
59-
closePromise,
60-
new Promise<void>((resolve) => setTimeout(resolve, 1_000)),
61-
]);
70+
throw new Error(failureMessage);
71+
}
6272

63-
if (process.exitCode === null && process.signalCode === null) {
64-
process.kill();
73+
async function terminateElectronApp(app: ElectronApplication): Promise<void> {
74+
const childProcess = app.process();
75+
76+
if (childProcess.exitCode !== null || childProcess.signalCode !== null) {
77+
return;
6578
}
6679

80+
const exitPromise = new Promise<void>((resolve) =>
81+
childProcess.once("exit", () => resolve())
82+
);
83+
childProcess.kill();
84+
6785
await Promise.race([
68-
closePromise,
6986
exitPromise,
7087
new Promise<void>((resolve) =>
7188
setTimeout(resolve, electronCleanupTimeoutMs)
7289
),
7390
]);
7491
}
7592

76-
test("published Windows release upgrades through the previous app", async ({
77-
browserName,
78-
}, testInfo) => {
79-
test.setTimeout(updateDownloadTimeoutMs + 6 * 60 * 1000);
80-
void browserName;
93+
function exitProcess(exitCode: number): never {
94+
// This standalone smoke test must not wait for Playwright to tear down an app that replaced itself.
95+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
96+
process.exit(exitCode);
97+
}
8198

99+
async function runPublishedUpgrade(): Promise<void> {
82100
const configuration = getUpgradeConfiguration();
83-
101+
const outputDirectory = path.resolve("test-results", "published-upgrade");
84102
const userDataDirectory = fs.mkdtempSync(
85103
path.join(os.tmpdir(), "fitfileviewer-published-upgrade-")
86104
);
87105
const mainProcessLogs: string[] = [];
88106
let electronApp: ElectronApplication | undefined;
89107

108+
fs.mkdirSync(outputDirectory, { recursive: true });
109+
90110
try {
91111
const environment = { ...process.env };
92112
delete environment.ELECTRON_RUN_AS_NODE;
@@ -112,34 +132,33 @@ test("published Windows release upgrades through the previous app", async ({
112132
const runningVersion = await electronApp.evaluate(({ app }) =>
113133
app.getVersion()
114134
);
115-
expect(runningVersion).toBe(configuration.fromVersion);
135+
assert.equal(runningVersion, configuration.fromVersion);
116136

117137
const expectedInstallerPath = path.join(
118138
configuration.updaterCachePath,
119139
"Fit-File-Viewer-nsis-x64-" + configuration.toVersion + ".exe"
120140
);
121-
await expect
122-
.poll(() => fs.existsSync(expectedInstallerPath), {
123-
timeout: updateDownloadTimeoutMs,
124-
})
125-
.toBe(true);
126-
await expect
127-
.poll(
128-
() =>
129-
electronApp?.evaluate(({ Menu }) => {
130-
const restartItem =
131-
Menu.getApplicationMenu()?.getMenuItemById(
132-
"restart-update"
133-
);
134-
return restartItem?.enabled === true;
135-
}),
136-
{ timeout: 30_000 }
137-
)
138-
.toBe(true);
141+
await waitFor(
142+
() => fs.existsSync(expectedInstallerPath),
143+
updateDownloadTimeoutMs,
144+
"The published update installer was not downloaded."
145+
);
146+
await waitFor(
147+
() =>
148+
electronApp?.evaluate(({ Menu }) => {
149+
const restartItem =
150+
Menu.getApplicationMenu()?.getMenuItemById(
151+
"restart-update"
152+
);
153+
return restartItem?.enabled === true;
154+
}) ?? false,
155+
30_000,
156+
"The restart-and-update menu item was not enabled."
157+
);
139158

140159
await page.screenshot({
141160
fullPage: true,
142-
path: testInfo.outputPath("update-downloaded.png"),
161+
path: path.join(outputDirectory, "update-downloaded.png"),
143162
});
144163

145164
const notification = page.locator("#notification");
@@ -160,19 +179,18 @@ test("published Windows release upgrades through the previous app", async ({
160179
});
161180
}
162181
);
163-
expect(restartTriggered).toBe(true);
164-
await expect
165-
.poll(
166-
() =>
167-
electronApp
168-
?.evaluate(
169-
({ BrowserWindow }) =>
170-
BrowserWindow.getAllWindows().length
171-
)
172-
.catch(() => 0),
173-
{ timeout: updateInstallHandoffTimeoutMs }
174-
)
175-
.toBe(0);
182+
assert.equal(restartTriggered, true);
183+
await waitFor(
184+
() =>
185+
electronApp
186+
?.evaluate(
187+
({ BrowserWindow }) =>
188+
BrowserWindow.getAllWindows().length === 0
189+
)
190+
.catch(() => true) ?? true,
191+
updateInstallHandoffTimeoutMs,
192+
"The old application window did not close for the update."
193+
);
176194

177195
fs.writeFileSync(
178196
configuration.evidencePath,
@@ -189,12 +207,19 @@ test("published Windows release upgrades through the previous app", async ({
189207
) + "\n"
190208
);
191209
} finally {
192-
await testInfo.attach("old-app-main-process.log", {
193-
body: Buffer.from(mainProcessLogs.join("\n")),
194-
contentType: "text/plain",
195-
});
210+
fs.writeFileSync(
211+
path.join(outputDirectory, "old-app-main-process.log"),
212+
mainProcessLogs.join("\n")
213+
);
196214
if (electronApp) {
197-
await closeElectronApp(electronApp);
215+
await terminateElectronApp(electronApp);
198216
}
199217
}
200-
});
218+
}
219+
220+
void runPublishedUpgrade()
221+
.then(() => exitProcess(0))
222+
.catch((error: unknown) => {
223+
console.error(error);
224+
exitProcess(1);
225+
});

0 commit comments

Comments
 (0)