From a22bf168f6b6006f265133bbfe7e7927dec93f0c Mon Sep 17 00:00:00 2001 From: Joe Roskopf <7951665+j-roskopf@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:26:15 -0600 Subject: [PATCH 1/5] ui test debugging --- .github/workflows/ui-tests.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 5f54c49..17ab104 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -33,11 +33,12 @@ jobs: - name: Build plugin and prepare sandbox run: ./gradlew prepareSandbox_runIdeForUiTests - # Linux runners have no display server; start a virtual one before launching the IDE. - - name: Start virtual display (Linux) + # Linux runners need a virtual display AND GUI toolkit libraries for the IDE to render. + - name: Install display dependencies (Linux) if: runner.os == 'Linux' run: | - sudo apt-get install -y xvfb + sudo apt-get update + sudo apt-get install -y xvfb libxrender1 libxtst6 libxi6 libxrandr2 libfreetype6 fontconfig Xvfb :99 -screen 0 1920x1080x24 & sleep 2 echo "DISPLAY=:99" >> "$GITHUB_ENV" @@ -57,16 +58,19 @@ jobs: IDE_PID=$! echo "IDE PID: $IDE_PID" - # Poll until the robot server responds to HTTP requests. + # Poll until the robot server port accepts connections. + # curl without -f returns 0 for ANY HTTP response (even 404), which is fine — + # we just need to know the server is listening. curl is available on all + # GitHub Actions runners (Linux, macOS, Windows). echo "Waiting for robot server on port 8082..." MAX_WAIT=90 ATTEMPT=0 - until curl -sf --connect-timeout 2 http://127.0.0.1:8082/api/about > /dev/null 2>&1; do + until curl -s --connect-timeout 2 -o /dev/null http://127.0.0.1:8082/; do ATTEMPT=$((ATTEMPT + 1)) if [ "$ATTEMPT" -ge "$MAX_WAIT" ]; then echo "ERROR: robot server did not start after $((MAX_WAIT * 5)) seconds" - echo "=== IDE output (last 100 lines) ===" - tail -100 ide-output.log 2>/dev/null || true + echo "=== IDE output (last 200 lines) ===" + tail -200 ide-output.log 2>/dev/null || true exit 1 fi echo " attempt $ATTEMPT/$MAX_WAIT — not ready yet, retrying in 5s..." From 552b0458b811de6194c0e242e302261f445f6a13 Mon Sep 17 00:00:00 2001 From: Joe Roskopf <7951665+j-roskopf@users.noreply.github.com> Date: Tue, 24 Feb 2026 06:47:42 -0600 Subject: [PATCH 2/5] ui debug 2 --- .../joetr/modulemaker/ModuleMakerUiTest.kt | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt index 5ad00a5..bcb3324 100644 --- a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt +++ b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt @@ -34,12 +34,39 @@ class ModuleMakerUiTest { @Test fun `opens via Find Action and creates repository module`() { with(remoteRobot) { - step("Wait for IDE frame to load") { + step("Wait for IDE to be ready and dismiss blocking dialogs") { waitFor(duration = Duration.ofMinutes(3), interval = Duration.ofSeconds(2)) { dismissBlockingDialogs() - findAll( + + // Check if the project frame is open + val ideFrame = findAll( byXpath("//div[@class='IdeFrameImpl']") - ).isNotEmpty() + ) + if (ideFrame.isNotEmpty()) { + println("Found IdeFrameImpl") + true + } else { + // Dump what top-level components exist so we can diagnose CI failures + val allComponents = findAll(byXpath("//div")) + val classNames = allComponents.mapNotNull { fixture -> + try { + fixture.callJs("component.getClass().getName()") + } catch (_: Exception) { + null + } + }.distinct() + println("Waiting for IdeFrameImpl... Found components: ${classNames.take(30)}") + + // If stuck on Welcome screen, the project didn't auto-open + val welcomeFrame = findAll( + byXpath("//div[@class='FlatWelcomeFrame']") + ) + if (welcomeFrame.isNotEmpty()) { + println("Detected Welcome screen - project did not auto-open") + } + + false + } } } From 9ff83dbca395d5fb272db1182f1d009948612397 Mon Sep 17 00:00:00 2001 From: Joe Roskopf <7951665+j-roskopf@users.noreply.github.com> Date: Tue, 24 Feb 2026 07:11:13 -0600 Subject: [PATCH 3/5] ui debug 3 --- .../com/joetr/modulemaker/ModuleMakerUiTest.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt index bcb3324..211293a 100644 --- a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt +++ b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt @@ -310,6 +310,23 @@ class ModuleMakerUiTest { // Any "Continue" or "Skip" buttons findAll(byXpath("//div[@text='Continue']")).firstOrNull()?.click() findAll(byXpath("//div[@text='Skip Remaining and Set Defaults']")).firstOrNull()?.click() + + // Catch-all: if a DialogWrapper dialog is blocking with any JButton, click it. + // This handles Android Studio-specific dialogs (setup wizard, EULA, etc.) + // that use non-standard button text. + val dialogButtons = findAll( + byXpath("//div[@class='MyDialog']//div[@class='JButton']") + ) + if (dialogButtons.isNotEmpty()) { + val btn = dialogButtons.first() + val btnText = try { + btn.callJs("component.getText()") + } catch (_: Exception) { + "unknown" + } + println("Dismissing blocking dialog by clicking button: '$btnText'") + btn.click() + } } private companion object { From 4ea4578e3dadb64727b6013db8793f57dd01721f Mon Sep 17 00:00:00 2001 From: Joe Roskopf <7951665+j-roskopf@users.noreply.github.com> Date: Tue, 24 Feb 2026 07:21:48 -0600 Subject: [PATCH 4/5] ui debug 3 --- .../com/joetr/modulemaker/ModuleMakerUiTest.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt index 211293a..1d7b385 100644 --- a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt +++ b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt @@ -311,21 +311,25 @@ class ModuleMakerUiTest { findAll(byXpath("//div[@text='Continue']")).firstOrNull()?.click() findAll(byXpath("//div[@text='Skip Remaining and Set Defaults']")).firstOrNull()?.click() - // Catch-all: if a DialogWrapper dialog is blocking with any JButton, click it. - // This handles Android Studio-specific dialogs (setup wizard, EULA, etc.) - // that use non-standard button text. + // Catch-all: if a DialogWrapper dialog is blocking, find buttons and click + // a safe one. Skip "Cancel"/"No"/"Exit" to avoid killing legitimate operations. val dialogButtons = findAll( byXpath("//div[@class='MyDialog']//div[@class='JButton']") ) - if (dialogButtons.isNotEmpty()) { - val btn = dialogButtons.first() + for (btn in dialogButtons) { val btnText = try { - btn.callJs("component.getText()") + btn.callJs("component.getText()")?.trim() ?: "" } catch (_: Exception) { - "unknown" + "" + } + val lower = btnText.lowercase() + if (lower in listOf("cancel", "no", "exit", "abort", "stop")) { + println("Skipping dangerous dialog button: '$btnText'") + continue } println("Dismissing blocking dialog by clicking button: '$btnText'") btn.click() + break } } From 6f58694a1a0534d6c13dcc03d743d9b22bda9348 Mon Sep 17 00:00:00 2001 From: Joe Roskopf <7951665+j-roskopf@users.noreply.github.com> Date: Tue, 24 Feb 2026 07:53:36 -0600 Subject: [PATCH 5/5] ui debug 4 --- .../joetr/modulemaker/ModuleMakerUiTest.kt | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt index 1d7b385..7854fac 100644 --- a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt +++ b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt @@ -70,28 +70,59 @@ class ModuleMakerUiTest { } } - step("Open Module Maker via Find Action") { - find( - byXpath("//div[@class='IdeFrameImpl']"), - Duration.ofSeconds(10) - ).click() + step("Wait for IDE to settle after loading") { + // On CI the IDE may still be indexing or initializing after the frame appears. + // Give it time before sending hotkeys. + Thread.sleep(10_000) + // Dismiss any dialogs that appeared during loading + dismissBlockingDialogs() + Thread.sleep(2_000) + } + step("Open Module Maker via Find Action") { val isMac = System.getProperty("os.name").contains("Mac", ignoreCase = true) - keyboard { - if (isMac) { - hotKey(KeyEvent.VK_META, KeyEvent.VK_SHIFT, KeyEvent.VK_A) + + // Retry the Find Action flow — on CI the first attempt may fail + // if the IDE hasn't fully initialized its action system. + waitFor(duration = Duration.ofSeconds(60), interval = Duration.ofSeconds(5)) { + // Click the IDE frame to ensure it has focus + find( + byXpath("//div[@class='IdeFrameImpl']"), + Duration.ofSeconds(10) + ).click() + Thread.sleep(500) + + keyboard { + if (isMac) { + hotKey(KeyEvent.VK_META, KeyEvent.VK_SHIFT, KeyEvent.VK_A) + } else { + hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_SHIFT, KeyEvent.VK_A) + } + } + Thread.sleep(2_000) + + // Check if Find Action popup appeared + val searchField = findAll( + byXpath("//div[@class='SearchEverywhereUI']") + ) + if (searchField.isEmpty()) { + println("Find Action popup not found, retrying...") + // Press Escape to clean up any partial state + keyboard { hotKey(KeyEvent.VK_ESCAPE) } + Thread.sleep(1_000) + false } else { - hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_SHIFT, KeyEvent.VK_A) + println("Find Action popup appeared") + keyboard { enterText("Module Maker") } + Thread.sleep(1_000) + keyboard { hotKey(KeyEvent.VK_ENTER) } + true } } - Thread.sleep(1_000) - keyboard { enterText("Module Maker") } - Thread.sleep(500) - keyboard { hotKey(KeyEvent.VK_ENTER) } } step("Verify Module Maker dialog opened") { - waitFor(duration = Duration.ofSeconds(15)) { + waitFor(duration = Duration.ofSeconds(30)) { findAll( byXpath("//div[@title='Module Maker']") ).isNotEmpty()