1+ import { normalize } from 'node:path' ;
2+ import { errors } from '@appium/base-driver' ;
13import { NovaWindowsDriver } from '../driver' ;
2- import { PSString , pwsh$ } from '../powershell' ;
4+ import { PSString , pwsh , pwsh$ } from '../powershell' ;
5+ import { MODIFY_FS_FEATURE } from '../constants' ;
6+ import { isUwpAppId , sleep } from '../util' ;
7+
8+ const TERMINATE_POLL_INTERVAL_MS = 200 ;
9+ const TERMINATE_TIMEOUT_MS = 10_000 ;
310
411const GET_SYSTEM_TIME_COMMAND = pwsh$ /* ps1 */ `(Get-Date).ToString(${ 0 } )` ;
512const ISO_8061_FORMAT = 'yyyy-MM-ddTHH:mm:sszzz' ;
@@ -9,33 +16,257 @@ export async function getDeviceTime(this: NovaWindowsDriver, _sessionId?: string
916 return await this . sendPowerShellCommand ( GET_SYSTEM_TIME_COMMAND . format ( fmt ) ) ;
1017}
1118
12- // command: 'hideKeyboard'
13- // payloadParams: { optional: ['strategy', 'key', 'keyCode', 'keyName'] }
19+ // ─── File operations ─────────────────────────────────────────────────────────
1420
15- // command: 'isKeyboardShown'
21+ const PUSH_FILE_COMMAND = pwsh$ /* ps1 */ `
22+ $path = ${ 0 }
23+ $parentDir = [IO.Path]::GetDirectoryName($path)
24+ if ($parentDir) { [IO.Directory]::CreateDirectory($parentDir) | Out-Null }
25+ [IO.File]::WriteAllBytes($path, [Convert]::FromBase64String(${ 1 } ))
26+ ` ;
1627
17- // command: 'pushFile'
18- // payloadParams: { required: ['path', 'data'] }
28+ export async function pushFile ( this : NovaWindowsDriver , path : string , data : string ) : Promise < void > {
29+ this . assertFeatureEnabled ( MODIFY_FS_FEATURE ) ;
30+ if ( ! path ) { throw new errors . InvalidArgumentError ( "'path' must be provided." ) ; }
31+ if ( ! data ) { throw new errors . InvalidArgumentError ( "'data' must be provided." ) ; }
32+ await this . sendPowerShellCommand (
33+ PUSH_FILE_COMMAND . format ( new PSString ( path ) . toString ( ) , new PSString ( data ) . toString ( ) )
34+ ) ;
35+ }
1936
20- // command: 'pullFile'
21- // payloadParams: { required: ['path'] }
37+ const PULL_FILE_COMMAND = pwsh$ /* ps1 */ `[Convert]::ToBase64String([IO.File]::ReadAllBytes(${ 0 } ))` ;
2238
23- // command: 'pullFolder'
24- // payloadParams: { required: ['path'] }
39+ export async function pullFile ( this : NovaWindowsDriver , path : string ) : Promise < string > {
40+ this . assertFeatureEnabled ( MODIFY_FS_FEATURE ) ;
41+ if ( ! path ) { throw new errors . InvalidArgumentError ( "'path' must be provided." ) ; }
42+ return await this . sendPowerShellCommand ( PULL_FILE_COMMAND . format ( new PSString ( path ) . toString ( ) ) ) ;
43+ }
2544
26- // # APP MANAGEMENT
45+ const PULL_FOLDER_COMMAND = pwsh$ /* ps1 */ `
46+ $srcPath = ${ 0 }
47+ $tempZip = [IO.Path]::GetTempFileName() + '.zip'
48+ try {
49+ Compress-Archive -LiteralPath $srcPath -DestinationPath $tempZip -ErrorAction Stop
50+ [Convert]::ToBase64String([IO.File]::ReadAllBytes($tempZip))
51+ } finally {
52+ if (Test-Path $tempZip) { Remove-Item $tempZip -Force }
53+ }
54+ ` ;
2755
28- // command: 'activateApp'
29- // payloadParams: { required: [['appId'], ['bundleId']], optional: ['options'] }
56+ export async function pullFolder ( this : NovaWindowsDriver , path : string ) : Promise < string > {
57+ this . assertFeatureEnabled ( MODIFY_FS_FEATURE ) ;
58+ if ( ! path ) { throw new errors . InvalidArgumentError ( "'path' must be provided." ) ; }
59+ return await this . sendPowerShellCommand ( PULL_FOLDER_COMMAND . format ( new PSString ( path ) . toString ( ) ) ) ;
60+ }
3061
31- // command: 'removeApp'
32- // payloadParams: { required: [['appId'], ['bundleId']], optional: ['options'] }
62+ // ─── Keyboard ────────────────────────────────────────────────────────────────
3363
34- //command: 'terminateApp'
35- // payloadParams: { required: [['appId'], ['bundleId']], optional: ['options'] }
64+ const HIDE_KEYBOARD_COMMAND = pwsh /* ps1 */ `
65+ $kb = Get-Process -Name 'TabTip','TextInputHost' -ErrorAction SilentlyContinue | Select-Object -First 1
66+ if ($null -eq $kb) { return }
67+ $kbEl = [System.Windows.Automation.AutomationElement]::RootElement.FindFirst(
68+ [System.Windows.Automation.TreeScope]::Children,
69+ [System.Windows.Automation.PropertyCondition]::new(
70+ [System.Windows.Automation.AutomationElement]::ProcessIdProperty,
71+ $kb.Id
72+ )
73+ )
74+ if ($null -ne $kbEl) {
75+ try {
76+ $kbEl.GetCurrentPattern([System.Windows.Automation.WindowPattern]::Pattern).Close()
77+ } catch {
78+ Stop-Process -Id $kb.Id -Force -ErrorAction SilentlyContinue
79+ }
80+ } else {
81+ Stop-Process -Id $kb.Id -Force -ErrorAction SilentlyContinue
82+ }
83+ ` ;
84+
85+ export async function hideKeyboard (
86+ this : NovaWindowsDriver ,
87+ _strategy ?: string ,
88+ _key ?: string ,
89+ _keyCode ?: string ,
90+ _keyName ?: string
91+ ) : Promise < void > {
92+ await this . sendPowerShellCommand ( HIDE_KEYBOARD_COMMAND ) ;
93+ }
94+
95+ const IS_KEYBOARD_SHOWN_COMMAND = pwsh /* ps1 */ `
96+ $kb = Get-Process -Name 'TabTip','TextInputHost' -ErrorAction SilentlyContinue | Select-Object -First 1
97+ if ($null -eq $kb) { Write-Output 'false'; return }
98+ $kbEl = [System.Windows.Automation.AutomationElement]::RootElement.FindFirst(
99+ [System.Windows.Automation.TreeScope]::Children,
100+ [System.Windows.Automation.PropertyCondition]::new(
101+ [System.Windows.Automation.AutomationElement]::ProcessIdProperty,
102+ $kb.Id
103+ )
104+ )
105+ if ($null -eq $kbEl) { Write-Output 'false'; return }
106+ if ($kbEl.GetCurrentPropertyValue([System.Windows.Automation.AutomationElement]::IsOffscreenProperty)) {
107+ Write-Output 'false'
108+ } else {
109+ Write-Output 'true'
110+ }
111+ ` ;
112+
113+ export async function isKeyboardShown ( this : NovaWindowsDriver ) : Promise < boolean > {
114+ const result = await this . sendPowerShellCommand ( IS_KEYBOARD_SHOWN_COMMAND ) ;
115+ return result . trim ( ) . toLowerCase ( ) === 'true' ;
116+ }
117+
118+ // ─── App management ──────────────────────────────────────────────────────────
119+
120+ export async function activateApp (
121+ this : NovaWindowsDriver ,
122+ appId : string ,
123+ _options ?: Record < string , unknown >
124+ ) : Promise < void > {
125+ if ( ! appId ) { throw new errors . InvalidArgumentError ( "'appId' or 'bundleId' must be provided." ) ; }
126+
127+ const isUwp = isUwpAppId ( appId ) ;
128+ if ( isUwp ) {
129+ await this . changeRootElement ( appId ) ;
130+ return ;
131+ }
132+
133+ const normalizedPath = normalize ( appId ) ;
134+ const parts = normalizedPath . toLowerCase ( ) . split ( '\\' ) . flatMap ( ( x ) => x . split ( '/' ) ) ;
135+ const executable = parts [ parts . length - 1 ] ;
136+ const processName = ( executable . endsWith ( '.exe' ) ? executable . slice ( 0 , - 4 ) : executable ) . replace ( / ' / g, "''" ) ;
137+
138+ const pidResult = await this . sendPowerShellCommand (
139+ /* ps1 */ `(Get-Process -Name '${ processName } ' -ErrorAction SilentlyContinue | Sort-Object StartTime -Descending | Select-Object -First 1).Id`
140+ ) ;
141+ const existingPid = Number ( pidResult . trim ( ) ) ;
36142
37- // command: 'isAppInstalled'
38- // payloadParams: { required: [['appId'], ['bundleId']] }
143+ if ( ! isNaN ( existingPid ) && existingPid > 0 ) {
144+ const handleResult = await this . sendPowerShellCommand (
145+ /* ps1 */ `(Get-Process -Id ${ existingPid } -ErrorAction SilentlyContinue).MainWindowHandle`
146+ ) ;
147+ const handle = Number ( handleResult . trim ( ) ) ;
148+ if ( ! isNaN ( handle ) && handle > 0 ) {
149+ await this . changeRootElement ( handle ) ;
150+ return ;
151+ }
152+ await this . attachToApplicationWindow ( [ existingPid ] ) ;
153+ return ;
154+ }
155+
156+ await this . changeRootElement ( appId ) ;
157+ }
158+
159+ export async function terminateApp (
160+ this : NovaWindowsDriver ,
161+ appId : string ,
162+ _options ?: Record < string , unknown >
163+ ) : Promise < boolean > {
164+ if ( ! appId ) { throw new errors . InvalidArgumentError ( "'appId' or 'bundleId' must be provided." ) ; }
165+
166+ const isUwp = isUwpAppId ( appId ) ;
167+
168+ let killed : boolean ;
169+ if ( isUwp ) {
170+ const safeFamily = new PSString ( appId . split ( '!' ) [ 0 ] ) . toString ( ) ;
171+ const checkResult = await this . sendPowerShellCommand (
172+ /* ps1 */ `
173+ $pkg = Get-AppxPackage | Where-Object { $_.PackageFamilyName -eq ${ safeFamily } }
174+ if ($null -eq $pkg) { Write-Output 'none'; return }
175+ $procs = Get-Process | Where-Object { $_.Path -like ($pkg.InstallLocation + '\\*') }
176+ if (@($procs).Count -eq 0) { Write-Output 'none' } else { ($procs | Select-Object -ExpandProperty Id) -join ',' }
177+ `
178+ ) ;
179+ const pids = checkResult . trim ( ) ;
180+ if ( pids === 'none' || pids === '' ) {
181+ await this . sendPowerShellCommand ( /* ps1 */ `$rootElement = $null` ) . catch ( ( ) => { } ) ;
182+ return false ;
183+ }
184+ await this . sendPowerShellCommand (
185+ /* ps1 */ `Stop-Process -Id ${ pids } -Force -ErrorAction SilentlyContinue`
186+ ) ;
187+
188+ const deadline = Date . now ( ) + TERMINATE_TIMEOUT_MS ;
189+ killed = false ;
190+ while ( Date . now ( ) < deadline ) {
191+ await sleep ( TERMINATE_POLL_INTERVAL_MS ) ;
192+ const stillRunning = await this . sendPowerShellCommand (
193+ /* ps1 */ `
194+ $pkg = Get-AppxPackage | Where-Object { $_.PackageFamilyName -eq ${ safeFamily } }
195+ if ($null -eq $pkg) { 'false' } else { ($null -ne (Get-Process | Where-Object { $_.Path -like ($pkg.InstallLocation + '\\*') } | Select-Object -First 1)).ToString().ToLower() }
196+ `
197+ ) ;
198+ if ( stillRunning . trim ( ) . toLowerCase ( ) !== 'true' ) {
199+ killed = true ;
200+ break ;
201+ }
202+ }
203+ } else {
204+ const normalizedPath = normalize ( appId ) ;
205+ const parts = normalizedPath . toLowerCase ( ) . split ( '\\' ) . flatMap ( ( x ) => x . split ( '/' ) ) ;
206+ const executable = parts [ parts . length - 1 ] ;
207+ const processName = ( executable . endsWith ( '.exe' ) ? executable . slice ( 0 , - 4 ) : executable ) . replace ( / ' / g, "''" ) ;
208+
209+ const checkResult = await this . sendPowerShellCommand (
210+ /* ps1 */ `$procs = Get-Process -Name '${ processName } ' -ErrorAction SilentlyContinue; if (@($procs).Count -eq 0) { Write-Output 'none' } else { ($procs | Select-Object -ExpandProperty Id) -join ',' }`
211+ ) ;
212+ const pids = checkResult . trim ( ) ;
213+ if ( pids === 'none' || pids === '' ) {
214+ await this . sendPowerShellCommand ( /* ps1 */ `$rootElement = $null` ) . catch ( ( ) => { } ) ;
215+ return false ;
216+ }
217+ await this . sendPowerShellCommand (
218+ /* ps1 */ `Stop-Process -Id ${ pids } -Force -ErrorAction SilentlyContinue`
219+ ) ;
220+
221+ const deadline = Date . now ( ) + TERMINATE_TIMEOUT_MS ;
222+ killed = false ;
223+ while ( Date . now ( ) < deadline ) {
224+ await sleep ( TERMINATE_POLL_INTERVAL_MS ) ;
225+ const stillRunning = await this . sendPowerShellCommand (
226+ /* ps1 */ `(Get-Process -Name '${ processName } ' -ErrorAction SilentlyContinue).Count -gt 0`
227+ ) ;
228+ if ( stillRunning . trim ( ) . toLowerCase ( ) !== 'true' ) {
229+ killed = true ;
230+ break ;
231+ }
232+ }
233+ }
234+
235+ await this . sendPowerShellCommand ( /* ps1 */ `$rootElement = $null` ) . catch ( ( ) => { } ) ;
236+ return killed ;
237+ }
238+
239+ export async function isAppInstalled ( this : NovaWindowsDriver , appId : string ) : Promise < boolean > {
240+ if ( ! appId ) { throw new errors . InvalidArgumentError ( "'appId' or 'bundleId' must be provided." ) ; }
241+
242+ const isUwp = isUwpAppId ( appId ) ;
243+ if ( isUwp ) {
244+ const safeFamily = new PSString ( appId . split ( '!' ) [ 0 ] ) . toString ( ) ;
245+ const result = await this . sendPowerShellCommand (
246+ /* ps1 */ `if (@(Get-AppxPackage | Where-Object { $_.PackageFamilyName -eq ${ safeFamily } }).Count -gt 0) { 'true' } else { 'false' }`
247+ ) ;
248+ return result . trim ( ) . toLowerCase ( ) === 'true' ;
249+ }
250+
251+ const hasPathSeparator = appId . includes ( '/' ) || appId . includes ( '\\' ) ;
252+ if ( hasPathSeparator ) {
253+ const safePath = new PSString ( appId ) . toString ( ) ;
254+ const result = await this . sendPowerShellCommand (
255+ /* ps1 */ `if (Test-Path -LiteralPath ${ safePath } ) { 'true' } else { 'false' }`
256+ ) ;
257+ return result . trim ( ) . toLowerCase ( ) === 'true' ;
258+ }
259+
260+ // Bare process name — search PATH
261+ const safeName = new PSString ( appId ) . toString ( ) ;
262+ const result = await this . sendPowerShellCommand (
263+ /* ps1 */ `if (Get-Command -Name ${ safeName } -ErrorAction SilentlyContinue) { 'true' } else { 'false' }`
264+ ) ;
265+ return result . trim ( ) . toLowerCase ( ) === 'true' ;
266+ }
39267
40268// command: 'installApp'
41269// payloadParams: { required: ['appPath'], optional: ['options'] }
270+
271+ // command: 'removeApp'
272+ // payloadParams: { required: [['appId'], ['bundleId']], optional: ['options'] }
0 commit comments