Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions .github/workflows/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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..."
Expand Down
113 changes: 96 additions & 17 deletions src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentFixture>(

// Check if the project frame is open
val ideFrame = findAll<ComponentFixture>(
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<ComponentFixture>(byXpath("//div"))
val classNames = allComponents.mapNotNull { fixture ->
try {
fixture.callJs<String>("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<ComponentFixture>(
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<ComponentFixture>(
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<ComponentFixture>(
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<ComponentFixture>(
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<ComponentFixture>(
byXpath("//div[@title='Module Maker']")
).isNotEmpty()
Expand Down Expand Up @@ -283,6 +341,27 @@ class ModuleMakerUiTest {
// Any "Continue" or "Skip" buttons
findAll<ComponentFixture>(byXpath("//div[@text='Continue']")).firstOrNull()?.click()
findAll<ComponentFixture>(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<JButtonFixture>(
byXpath("//div[@class='MyDialog']//div[@class='JButton']")
)
for (btn in dialogButtons) {
val btnText = try {
btn.callJs<String>("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 {
Expand Down
Loading