Skip to content

Commit ed16051

Browse files
authored
ui test running on ci (#84)
1 parent a439544 commit ed16051

2 files changed

Lines changed: 107 additions & 24 deletions

File tree

.github/workflows/ui-tests.yml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ jobs:
3333
- name: Build plugin and prepare sandbox
3434
run: ./gradlew prepareSandbox_runIdeForUiTests
3535

36-
# Linux runners have no display server; start a virtual one before launching the IDE.
37-
- name: Start virtual display (Linux)
36+
# Linux runners need a virtual display AND GUI toolkit libraries for the IDE to render.
37+
- name: Install display dependencies (Linux)
3838
if: runner.os == 'Linux'
3939
run: |
40-
sudo apt-get install -y xvfb
40+
sudo apt-get update
41+
sudo apt-get install -y xvfb libxrender1 libxtst6 libxi6 libxrandr2 libfreetype6 fontconfig
4142
Xvfb :99 -screen 0 1920x1080x24 &
4243
sleep 2
4344
echo "DISPLAY=:99" >> "$GITHUB_ENV"
@@ -57,16 +58,19 @@ jobs:
5758
IDE_PID=$!
5859
echo "IDE PID: $IDE_PID"
5960
60-
# Poll until the robot server responds to HTTP requests.
61+
# Poll until the robot server port accepts connections.
62+
# curl without -f returns 0 for ANY HTTP response (even 404), which is fine —
63+
# we just need to know the server is listening. curl is available on all
64+
# GitHub Actions runners (Linux, macOS, Windows).
6165
echo "Waiting for robot server on port 8082..."
6266
MAX_WAIT=90
6367
ATTEMPT=0
64-
until curl -sf --connect-timeout 2 http://127.0.0.1:8082/api/about > /dev/null 2>&1; do
68+
until curl -s --connect-timeout 2 -o /dev/null http://127.0.0.1:8082/; do
6569
ATTEMPT=$((ATTEMPT + 1))
6670
if [ "$ATTEMPT" -ge "$MAX_WAIT" ]; then
6771
echo "ERROR: robot server did not start after $((MAX_WAIT * 5)) seconds"
68-
echo "=== IDE output (last 100 lines) ==="
69-
tail -100 ide-output.log 2>/dev/null || true
72+
echo "=== IDE output (last 200 lines) ==="
73+
tail -200 ide-output.log 2>/dev/null || true
7074
exit 1
7175
fi
7276
echo " attempt $ATTEMPT/$MAX_WAIT — not ready yet, retrying in 5s..."

src/uiTest/kotlin/com/joetr/modulemaker/ModuleMakerUiTest.kt

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,37 +34,95 @@ class ModuleMakerUiTest {
3434
@Test
3535
fun `opens via Find Action and creates repository module`() {
3636
with(remoteRobot) {
37-
step("Wait for IDE frame to load") {
37+
step("Wait for IDE to be ready and dismiss blocking dialogs") {
3838
waitFor(duration = Duration.ofMinutes(3), interval = Duration.ofSeconds(2)) {
3939
dismissBlockingDialogs()
40-
findAll<ComponentFixture>(
40+
41+
// Check if the project frame is open
42+
val ideFrame = findAll<ComponentFixture>(
4143
byXpath("//div[@class='IdeFrameImpl']")
42-
).isNotEmpty()
44+
)
45+
if (ideFrame.isNotEmpty()) {
46+
println("Found IdeFrameImpl")
47+
true
48+
} else {
49+
// Dump what top-level components exist so we can diagnose CI failures
50+
val allComponents = findAll<ComponentFixture>(byXpath("//div"))
51+
val classNames = allComponents.mapNotNull { fixture ->
52+
try {
53+
fixture.callJs<String>("component.getClass().getName()")
54+
} catch (_: Exception) {
55+
null
56+
}
57+
}.distinct()
58+
println("Waiting for IdeFrameImpl... Found components: ${classNames.take(30)}")
59+
60+
// If stuck on Welcome screen, the project didn't auto-open
61+
val welcomeFrame = findAll<ComponentFixture>(
62+
byXpath("//div[@class='FlatWelcomeFrame']")
63+
)
64+
if (welcomeFrame.isNotEmpty()) {
65+
println("Detected Welcome screen - project did not auto-open")
66+
}
67+
68+
false
69+
}
4370
}
4471
}
4572

46-
step("Open Module Maker via Find Action") {
47-
find<ComponentFixture>(
48-
byXpath("//div[@class='IdeFrameImpl']"),
49-
Duration.ofSeconds(10)
50-
).click()
73+
step("Wait for IDE to settle after loading") {
74+
// On CI the IDE may still be indexing or initializing after the frame appears.
75+
// Give it time before sending hotkeys.
76+
Thread.sleep(10_000)
77+
// Dismiss any dialogs that appeared during loading
78+
dismissBlockingDialogs()
79+
Thread.sleep(2_000)
80+
}
5181

82+
step("Open Module Maker via Find Action") {
5283
val isMac = System.getProperty("os.name").contains("Mac", ignoreCase = true)
53-
keyboard {
54-
if (isMac) {
55-
hotKey(KeyEvent.VK_META, KeyEvent.VK_SHIFT, KeyEvent.VK_A)
84+
85+
// Retry the Find Action flow — on CI the first attempt may fail
86+
// if the IDE hasn't fully initialized its action system.
87+
waitFor(duration = Duration.ofSeconds(60), interval = Duration.ofSeconds(5)) {
88+
// Click the IDE frame to ensure it has focus
89+
find<ComponentFixture>(
90+
byXpath("//div[@class='IdeFrameImpl']"),
91+
Duration.ofSeconds(10)
92+
).click()
93+
Thread.sleep(500)
94+
95+
keyboard {
96+
if (isMac) {
97+
hotKey(KeyEvent.VK_META, KeyEvent.VK_SHIFT, KeyEvent.VK_A)
98+
} else {
99+
hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_SHIFT, KeyEvent.VK_A)
100+
}
101+
}
102+
Thread.sleep(2_000)
103+
104+
// Check if Find Action popup appeared
105+
val searchField = findAll<ComponentFixture>(
106+
byXpath("//div[@class='SearchEverywhereUI']")
107+
)
108+
if (searchField.isEmpty()) {
109+
println("Find Action popup not found, retrying...")
110+
// Press Escape to clean up any partial state
111+
keyboard { hotKey(KeyEvent.VK_ESCAPE) }
112+
Thread.sleep(1_000)
113+
false
56114
} else {
57-
hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_SHIFT, KeyEvent.VK_A)
115+
println("Find Action popup appeared")
116+
keyboard { enterText("Module Maker") }
117+
Thread.sleep(1_000)
118+
keyboard { hotKey(KeyEvent.VK_ENTER) }
119+
true
58120
}
59121
}
60-
Thread.sleep(1_000)
61-
keyboard { enterText("Module Maker") }
62-
Thread.sleep(500)
63-
keyboard { hotKey(KeyEvent.VK_ENTER) }
64122
}
65123

66124
step("Verify Module Maker dialog opened") {
67-
waitFor(duration = Duration.ofSeconds(15)) {
125+
waitFor(duration = Duration.ofSeconds(30)) {
68126
findAll<ComponentFixture>(
69127
byXpath("//div[@title='Module Maker']")
70128
).isNotEmpty()
@@ -283,6 +341,27 @@ class ModuleMakerUiTest {
283341
// Any "Continue" or "Skip" buttons
284342
findAll<ComponentFixture>(byXpath("//div[@text='Continue']")).firstOrNull()?.click()
285343
findAll<ComponentFixture>(byXpath("//div[@text='Skip Remaining and Set Defaults']")).firstOrNull()?.click()
344+
345+
// Catch-all: if a DialogWrapper dialog is blocking, find buttons and click
346+
// a safe one. Skip "Cancel"/"No"/"Exit" to avoid killing legitimate operations.
347+
val dialogButtons = findAll<JButtonFixture>(
348+
byXpath("//div[@class='MyDialog']//div[@class='JButton']")
349+
)
350+
for (btn in dialogButtons) {
351+
val btnText = try {
352+
btn.callJs<String>("component.getText()")?.trim() ?: ""
353+
} catch (_: Exception) {
354+
""
355+
}
356+
val lower = btnText.lowercase()
357+
if (lower in listOf("cancel", "no", "exit", "abort", "stop")) {
358+
println("Skipping dangerous dialog button: '$btnText'")
359+
continue
360+
}
361+
println("Dismissing blocking dialog by clicking button: '$btnText'")
362+
btn.click()
363+
break
364+
}
286365
}
287366

288367
private companion object {

0 commit comments

Comments
 (0)