@@ -5,13 +5,58 @@ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promis
55import os from "node:os" ;
66import path from "node:path" ;
77import test from "node:test" ;
8+ import { promisify } from "node:util" ;
89import { createJiti } from "jiti" ;
910import { MakerNsis } from "../maker-nsis" ;
1011import { MakerWix } from "@electron-forge/maker-wix" ;
1112
1213// Load the configuration with the same TypeScript loader used by Electron Forge.
1314const configPromise = createJiti ( import . meta. url ) . import < ForgeConfig > ( "../forge.config.ts" , { default : true } ) ;
1415
16+ test (
17+ "DMG cleanup retries the device after the volume has been unmounted" ,
18+ { skip : process . platform !== "darwin" } ,
19+ async ( context ) => {
20+ const require = createRequire ( import . meta. url ) ;
21+ const installerRequire = createRequire ( require . resolve ( "electron-installer-dmg" ) ) ;
22+ const appdmgRequire = createRequire ( installerRequire . resolve ( "appdmg" ) ) ;
23+ const appdmg = installerRequire ( "appdmg" ) ;
24+ const Pipeline = appdmgRequire ( "./lib/pipeline" ) ;
25+ const util = appdmgRequire ( "./lib/util" ) ;
26+
27+ // Exercise the installed dependency's mount/cleanup steps without mounting disks.
28+ context . mock . method ( Pipeline . prototype , "run" , function ( ) {
29+ return this ;
30+ } ) ;
31+ for ( const stdout of [
32+ "/dev/disk4\tApple_partition_scheme\n/dev/disk4s1\tApple_HFS\t/Volumes/JumpServer Test\n" ,
33+ "/dev/disk4\tGUID_partition_scheme\n/dev/disk4s2\tApple_APFS\n/dev/disk5\tEF57347C-0000-11AA-AA11-00306543ECAC\n/dev/disk5s1\t41504653-0000-11AA-AA11-00306543ECAC\t/Volumes/JumpServer Test\n"
34+ ] ) {
35+ const detachTargets : string [ ] = [ ] ;
36+ context . mock . method ( util , "sh" , ( _command , args , callback ) => {
37+ if ( args [ 0 ] === "attach" ) return callback ( null , { stdout } ) ;
38+ assert . equal ( args [ 0 ] , "detach" ) ;
39+ detachTargets . push ( args [ 1 ] ) ;
40+ if ( detachTargets . length === 1 ) {
41+ return callback ( Object . assign ( new Error ( "Resource busy after unmount" ) , { code : 16 } ) ) ;
42+ }
43+ if ( args [ 1 ] . startsWith ( "/Volumes/" ) ) {
44+ return callback ( Object . assign ( new Error ( "No such file or directory" ) , { code : 2 } ) ) ;
45+ }
46+ callback ( null ) ;
47+ } ) ;
48+ const pipeline = appdmg ( { target : "unused.dmg" , basepath : "." , specification : { } } ) ;
49+ await promisify ( pipeline . steps . find ( ( step ) => step . title === "Mounting temporary image" ) . fn ) ( ) ;
50+ await promisify ( pipeline . steps . find ( ( step ) => step . title === "Unmounting temporary image" ) . fn ) ( ) ;
51+ assert . deepEqual ( detachTargets , [ "/dev/disk4" , "/dev/disk4" ] ) ;
52+ }
53+
54+ const denied = Object . assign ( new Error ( "Permission denied" ) , { code : 13 } ) ;
55+ context . mock . method ( util , "sh" , ( _command , _args , callback ) => callback ( denied ) ) ;
56+ await assert . rejects ( promisify ( appdmgRequire ( "./lib/hdiutil" ) . detach ) ( "/dev/disk4" ) , denied ) ;
57+ }
58+ ) ;
59+
1560test ( "NSIS builder resolves the download cache API required on clean machines" , ( ) => {
1661 const require = createRequire ( import . meta. url ) ;
1762 const builderRequire = createRequire ( require . resolve ( "app-builder-lib" ) ) ;
0 commit comments