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..." diff --git a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt index 5ad00a5..7854fac 100644 --- a/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt +++ b/src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt @@ -34,37 +34,95 @@ 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 + } } } - 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() @@ -283,6 +341,27 @@ 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, 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']") + ) + for (btn in dialogButtons) { + val btnText = try { + btn.callJs("component.getText()")?.trim() ?: "" + } catch (_: Exception) { + "" + } + 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 + } } private companion object {