1- import { expect , test , _electron as electron } from "@playwright/test" ;
2- import type { ElectronApplication } from "@playwright/test" ;
1+ import assert from "node:assert/strict" ;
32import fs from "node:fs" ;
43import os from "node:os" ;
54import path from "node:path" ;
5+ import { _electron as electron } from "@playwright/test" ;
6+ import type { ElectronApplication } from "@playwright/test" ;
67
78type 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