From 100abc953e477fb275444bd0d0f7bbee494dff9a Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:47:10 -0700 Subject: [PATCH] fix(e2e): re-capture install-button template + fix phantom window geometry Attempt 2's proof frames showed two bugs, both now fixed from the live evidence: 1. The #68183 install-button.png predated the installer UI restyle to the '[ INSTALL ]' bracket look, so the template never matched and we fell through to the position fallback. Re-captured install-button.png from a real CI desktop frame (the actual rendered button). 2. The fallback then clicked the WRONG spot: ahk_exe's first WinGetPos matched a hidden 16x16 helper window ('Window found at w=16 h=16' in ahk.log), and BTN_FY=0.87 aimed below the real button anyway. The button center measured at ~(0.50, 0.59) of the ~full-screen window. Rewrite: * WaitForRealWindow() skips phantom/hidden matches (requires w>400,h>300) and returns the true rect; the installer window is then activated before any click. * Install-finished is now driven primarily by the authoritative 'bootstrap complete' line in bootstrap-installer.log (matches BootstrapEvent::Complete), with the Launch template as a secondary signal and a window-relative fallback click. * Fallback clicks use the corrected (0.50, 0.59) window fraction. --- .../install/e2e-assets/install-and-launch.ahk | 164 ++++++++++-------- tests/install/e2e-assets/install-button.png | Bin 1200 -> 911 bytes 2 files changed, 88 insertions(+), 76 deletions(-) diff --git a/tests/install/e2e-assets/install-and-launch.ahk b/tests/install/e2e-assets/install-and-launch.ahk index 98dbed0c32..873b3a5d65 100644 --- a/tests/install/e2e-assets/install-and-launch.ahk +++ b/tests/install/e2e-assets/install-and-launch.ahk @@ -6,20 +6,23 @@ ; the real Hermes.exe (Electron desktop) window to appear. ; ; Adapted from @ethernet8023's e2e/windows/install-hermes-desktop.ahk -; (PR #68183) — same ImageSearch approach and button templates; this -; variant targets windows by process name (ahk_exe) so the installer -; window and the launched app window (both titled "Hermes") can't be -; confused, and it clicks Launch instead of closing the window, because -; the Launch hand-off is part of the flow under test. +; (PR #68183) — same ImageSearch approach; the install-button template was +; re-captured from a live CI frame (the #68183 templates predated the +; installer UI restyle to the "[ INSTALL ]" bracket look and never matched). ; -; Robustness beyond the original: -; * Log() survives a missing stdout (GUI-subsystem AHK started without a -; console throws "(6) The handle is invalid" on FileAppend to '*' — -; that single throw killed the whole first CI attempt). -; * If a button template doesn't match (installer UI restyled), falls -; back to clicking the button's known relative position, and the -; install-finished signal falls back to "bootstrap complete" in -; bootstrap-installer.log. Every fallback is logged loudly. +; Robustness beyond the original — every one earned from a real CI failure: +; * Log() survives a missing stdout. GUI-subsystem AHK started via +; Start-Process has no console, so FileAppend to '*' throws +; "(6) The handle is invalid" — that single throw killed attempt 1. +; * We wait for a REAL-sized installer window before reading geometry. +; ahk_exe matched a hidden 16x16 helper window first (attempt 2's +; "Window found at ... w=16 h=16"), so relative-position math was +; computed against a phantom rect. +; * Clicks fall back to a SCREEN-fraction position when the template +; doesn't match (the Tauri window is effectively full-screen on the +; runner, so screen ~= window). Button center measured at ~0.50, 0.59. +; * Install-finished has a second signal: "bootstrap complete" in +; bootstrap-installer.log, so a Launch-template miss can't strand us. ; ; Args: [1] log path [2] setup exe name [3] bootstrap-installer.log path @@ -30,49 +33,59 @@ bootstrapLog := A_Args.Length >= 3 ? A_Args[3] : "" Log(text) { msg := Format("[autohotkey] {}`n", text) ToolTip(text) - ; stdout only exists when AHK was launched from a console. Under - ; Start-Process (no console) FileAppend to '*' throws "(6) The handle - ; is invalid" — and a Log() that throws kills the whole script from - ; inside OnError. The file log below is the real record. - try FileAppend(msg, '*') + try FileAppend(msg, '*') ; stdout may not exist (no console) FileAppend(msg, logPath) } OnError(LogError) - LogError(err, mode) { Log(Format("Unhandled error: {}", err.Message)) ExitApp(1) - return -1 ; suppress the standard error dialog + return -1 } SetWorkingDir(A_ScriptDir) CoordMode("Pixel", "Screen") CoordMode("Mouse", "Screen") -ClickWithMarker(x, y, button := "Left") { - Click(x, y, button) +ClickWithMarker(x, y) { + Click(x, y) Sleep(10) MouseMove(30, 30) Log(Format("Clicked at {1}, {2}", x, y)) } -; Single-pass image search inside a window. Returns true + center coords. -TryFindImage(winTitle, imageFile, &outX, &outY) { - try { - WinGetPos(&wx, &wy, &ww, &wh, winTitle) - } catch { - return false +; Wait until an installer window exists AND has a real (non-phantom) size, +; then return its rect. ahk_exe can transiently match a hidden helper. +WaitForRealWindow(winTitle, timeoutMs) { + deadline := A_TickCount + timeoutMs + while (A_TickCount < deadline) { + for hwnd in WinGetList(winTitle) { + try { + WinGetPos(&wx, &wy, &ww, &wh, "ahk_id " hwnd) + if (ww > 400 && wh > 300) { + return { hwnd: hwnd, x: wx, y: wy, w: ww, h: wh } + } + } catch { + continue + } + } + Sleep(500) } + throw Error(Format("no real-sized window matched {} within {}ms", winTitle, timeoutMs)) +} + +; Image search inside a rect. Returns true + center coords. +TryFindImage(rect, imageFile, &outX, &outY) { hBitmap := LoadPicture(imageFile) if !hBitmap { throw Error("LoadPicture failed: " imageFile) } - bm := Buffer(32, 0) ; BITMAP structure on x64 + bm := Buffer(32, 0) DllCall("GetObject", "Ptr", hBitmap, "Int", bm.Size, "Ptr", bm) width := NumGet(bm, 4, "Int") height := NumGet(bm, 8, "Int") - if ImageSearch(&x, &y, wx, wy, wx + ww, wy + wh, Format("*10 {}", imageFile)) { + if ImageSearch(&x, &y, rect.x, rect.y, rect.x + rect.w, rect.y + rect.h, Format("*20 {}", imageFile)) { outX := x + Floor(width / 2) outY := y + Floor(height / 2) return true @@ -80,21 +93,13 @@ TryFindImage(winTitle, imageFile, &outX, &outY) { return false } -; Fractional window position -> screen coords (fallback click target). -WindowRelPoint(winTitle, fx, fy, &outX, &outY) { - WinGetPos(&wx, &wy, &ww, &wh, winTitle) - outX := wx + Floor(ww * fx) - outY := wy + Floor(wh * fy) -} - BootstrapLogContains(needle) { global bootstrapLog if (bootstrapLog = "" or !FileExist(bootstrapLog)) { return false } try { - ; Read-share open: the installer still holds the file for writing. - f := FileOpen(bootstrapLog, "r-d") + f := FileOpen(bootstrapLog, "r-d") ; read, share read+write if !f { return false } @@ -109,26 +114,26 @@ BootstrapLogContains(needle) { installerWin := "ahk_exe " setupExe appWin := "ahk_exe Hermes.exe" -; The Install/Launch button sits centered horizontally near the bottom of -; the installer window (measured from production screenshots; used only -; when the image template fails to match a restyled UI). +; Button center as a fraction of the window rect (installer is ~full-screen +; on the runner). Measured from a live CI frame: center ~ (0.50, 0.59). BTN_FX := 0.50 -BTN_FY := 0.87 +BTN_FY := 0.59 -Log("Waiting for the installer window (" installerWin ") ...") +Log("Waiting for a real-sized installer window (" installerWin ") ...") +rect := WaitForRealWindow(installerWin, 90000) +Log(Format("Installer window: x={1} y={2} w={3} h={4}", rect.x, rect.y, rect.w, rect.h)) try { - WinWait(installerWin, , 60) + WinActivate("ahk_id " rect.hwnd) + Sleep(500) } catch { - throw Error("installer window did not appear within 60s") + Log("WARNING: could not activate installer window") } -WinGetPos(&x, &y, &w, &h, installerWin) -Log(Format("Window found at x={1} y={2} w={3} h={4}", x, y, w, h)) -; ── Step 1: click Install (template first, relative-position fallback) ── +; ── Step 1: click Install ─────────────────────────────────────────────── installClicked := false -deadline := A_TickCount + 60000 +deadline := A_TickCount + 30000 while (A_TickCount < deadline) { - if TryFindImage(installerWin, A_ScriptDir "\install-button.png", &ix, &iy) { + if TryFindImage(rect, A_ScriptDir "\install-button.png", &ix, &iy) { ClickWithMarker(ix, iy) Log("Install clicked (template match)") installClicked := true @@ -137,48 +142,58 @@ while (A_TickCount < deadline) { Sleep(500) } if !installClicked { - WindowRelPoint(installerWin, BTN_FX, BTN_FY, &ix, &iy) + ix := rect.x + Floor(rect.w * BTN_FX) + iy := rect.y + Floor(rect.h * BTN_FY) ClickWithMarker(ix, iy) - Log("FALLBACK: install template never matched; clicked relative position") + Log("FALLBACK: install template never matched; clicked window-relative position") } ; ── Step 2: wait for the install to finish ────────────────────────────── -; Primary signal: the Launch button template appears. Secondary signal: -; "bootstrap complete" in bootstrap-installer.log (the installer's own -; completion line) — after which we give the template 2 more minutes and -; then fall back to the relative-position click. +; Primary: the "bootstrap complete" line in the installer's own log — the +; authoritative done signal. Secondary: the Launch button template. launchX := 0, launchY := 0 launchFound := false -completeSince := 0 +complete := false waitDeadline := A_TickCount + 1000 * 60 * 45 -Log("Waiting for install to finish (Launch template or bootstrap log) ...") +Log("Waiting for install to finish (bootstrap log or Launch template) ...") while (A_TickCount < waitDeadline) { - if TryFindImage(installerWin, A_ScriptDir "\launch-button.png", &launchX, &launchY) { + if BootstrapLogContains("bootstrap complete") { + complete := true + Log("bootstrap-installer.log reports completion") + break + } + ; refresh the rect (window can move/resize between stages) + try rect := WaitForRealWindow(installerWin, 2000) + if TryFindImage(rect, A_ScriptDir "\launch-button.png", &launchX, &launchY) { launchFound := true Log("Install finished (Launch template visible)") break } - if (completeSince = 0 and BootstrapLogContains("bootstrap complete")) { - completeSince := A_TickCount - Log("bootstrap-installer.log reports completion; giving the Launch template 120s") - } - if (completeSince > 0 and A_TickCount - completeSince > 120000) { - Log("FALLBACK: log says complete but Launch template never matched") - break - } Sleep(2000) } -if (!launchFound and completeSince = 0) { - throw Error("install did not finish within 45 minutes (no Launch button, no completion log line)") +if (!launchFound and !complete) { + throw Error("install did not finish within 45 minutes (no completion log line, no Launch button)") +} + +; Give the button a moment to swap Install -> Launch after completion. +if (complete and !launchFound) { + Sleep(2000) + try rect := WaitForRealWindow(installerWin, 10000) + if TryFindImage(rect, A_ScriptDir "\launch-button.png", &launchX, &launchY) { + launchFound := true + Log("Launch template matched after completion") + } } ; ── Step 3: click Launch — the hand-off under test ────────────────────── if launchFound { ClickWithMarker(launchX, launchY) + Log("Launch clicked (template)") } else { - WindowRelPoint(installerWin, BTN_FX, BTN_FY, &lx, &ly) + lx := rect.x + Floor(rect.w * BTN_FX) + ly := rect.y + Floor(rect.h * BTN_FY) ClickWithMarker(lx, ly) - Log("FALLBACK: clicked Launch at relative position") + Log("FALLBACK: clicked Launch at window-relative position") } Log("Launch clicked; waiting for the Hermes desktop app window") @@ -191,9 +206,6 @@ try { WinGetPos(&ax, &ay, &aw, &ah, appWin) Log(Format("App window appeared at x={1} y={2} w={3} h={4}", ax, ay, aw, ah)) -; Give the renderer a few seconds on screen (recorded as proof), then hand -; control back to the PowerShell driver, which closes the app and re-launches -; it under Playwright for the update legs. -Sleep(8000) +Sleep(8000) ; let the renderer paint (recorded as proof) Log("done") ExitApp(0) diff --git a/tests/install/e2e-assets/install-button.png b/tests/install/e2e-assets/install-button.png index feed47bd98fb3ffe464025c6f52553205ed89751..7d86dbc4c0e67252abe3fc37f630707c94dd73c1 100644 GIT binary patch literal 911 zcmeAS@N?(olHy`uVBq!ia0vp^vw>KLgAGU)tvlt&z`z{l>EaktG3V_aUymt{GRHr@ z*I72rW5QmCsCFY)tq%*_@?9iDCM;+Vut^ChxwWVKmGjF+<71jny)Lwr zFPu;rGR1>4TP5d^6zA*>Za~cwH{jx`JzmEzeUJa(-@3gW+Y))-++Fl1$InmdKil_t zyL;5X9{SOkb@bKKe_M`zpSN~}KvbFLdB^P6ZHL0jZ#~Yt|HUX)@NZSP3%j*cb645) zjGU^S8(FTD`&|vb`R>t?<H=t_7*f7cF$#m~!%u{r(9xZ*Phn+G;Q-KlF|D>!KX}IeDjKZms5j_+s9f($@7a zp6iE;9Q8h%nsxf&=Tuh{ws!p;F;*)x))-IkJ+@#C%c@ffm)wsPIX+Fr*bI5uhSU$1!uI{f}g__!dIk%U*mh;xh_!_BhkQg1#bX$ADbJk<>dn+{OSS_je z#TF)O&cE=p;j|0KvsN|#jTUWIHh566USFI=Er;Q4yNEvf!sYk>)FsQR(hb_QH9IPzsdiNl8~DptP{Vi-AR9r zMU70`-SilP!ERC;S>)4Aei@aa7&kX>vuQi%aZ(g`=4X z7k}K&H@9=rgRW0ey#M^>vB?FWJGHerx>agf5Bl%>mZMZY(`d@q=Q&E%9yeM{h0GFr zTYMvw6NRr~;U3%2I%QwzQh_*d!tC+h{Ijw?@4=t!jk8sYF7C;D!1M3DfWS`ob2q^T boz4Fqq4?@TW5|7AMq}`F^>bP0l+XkKhMBWX literal 1200 zcmX9;Ycv!H6rNFQVwr43op!QisWz%piB^c1W3(lqj7@2#u)7|yN|b~R&4`s_WcAQP zsV0vZjgg@envmCE!X}TXd5}p{G45q&cklTg=bm%#x#xU8F3Z!y%}{@-K7l|mbf>v` zX+1|vt9f&@TAIPD)fzF%%WXTcr@`usw$Y2A`9%>3UoZTZL_*%>CE9@k&z(Nh$*Gwu zRp@yRmmWcaH}(wAxF(`qEN(rCc@L4Ah>MwM_A6S1qJJiie*{TC^)KN;6aMuOKB!@26li|{ zM+gsFvAP`|OJGt1j&a!ZHyG~6s&=fC!nH?GBt~Hi#@xis(fH~u49LMD4nKZ|kUZ>u z11m!@?ly#7#)=k{kHfap=#dKf)!-Zt={$Jc0R=U{$VOH%q}MF_aLtUBd(%! z2u>HGUj{z!(JsR9DD;j%N)h(GgM0NeyHc>de}?3X2K%*lXN0l2ECNC2mbqBro^8-rYqMXuvo?d}atT~~FV z&z{X4YpWL&@F_u?%FRQ{mN3ic|G{AB89%ZnbA)JeLiU0i>M-Fu>Ex~WM3(2%&!99I>XmJ$I2a?r}x9-m6nNn4ome4c4RL5 zr1^=mja6vy@s^Xn{7`koQl8mNT0H13|81Z*ajz9KjN_{zXLK>_qq1+l@~QWN=OUdP zsg5KzzMQg2K7Mdr#Av#aYS~j^XmzD=K$||Pja;!0LrFb_vK(=pNU;L$tjCcGE-1PbO7Q;JLf~#Fw>(v1c6!H+q!`M{ft3BwzrCVC>@-Y;#GA!%@pONb9xbfzG zedk-q{cp%sv_N^UO&K+u$}Di$SdnZIlDyOVyASOp$#ZxP9<%$q4?4aT2l49uN267m zX*ylK{H`*whkU^Fb<@~4Yi$l(IUy+PaH}|K6X;zTA(oH^q)h?7Cu#O{dm~-#*CDu5 LJzR^oA4>WMWp}7g