Skip to content

Commit a439544

Browse files
authored
jewel migration (#83)
* jewel migration * ui tests
1 parent 0b46297 commit a439544

22 files changed

Lines changed: 795 additions & 335 deletions

.claude/settings.local.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(tail:*)",
5+
"Bash(grep:*)",
6+
"Bash(grep)",
7+
"Bash(chmod +x)",
8+
"Bash(lsof)",
9+
"Bash(grep*)",
10+
"Bash(jar tf:*)",
11+
"Bash(head:*)",
12+
"Bash(./gradlew*)"
13+
]
14+
}
15+
}

.github/workflows/ui-tests.yml

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
name: UI Tests
2+
3+
on:
4+
pull_request:
5+
branches: [ "main" ]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
ui-test:
12+
strategy:
13+
# Keep running the other platforms even if one fails so we get full signal
14+
fail-fast: false
15+
matrix:
16+
os: [ubuntu-latest, windows-latest, macos-latest]
17+
18+
runs-on: ${{ matrix.os }}
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Set up JDK 17
24+
uses: actions/setup-java@v4
25+
with:
26+
distribution: temurin
27+
java-version: 17
28+
29+
- name: Setup Gradle
30+
uses: gradle/actions/setup-gradle@v4
31+
32+
# Pre-download IDE and build plugin so the background launch is fast and doesn't fail silently.
33+
- name: Build plugin and prepare sandbox
34+
run: ./gradlew prepareSandbox_runIdeForUiTests
35+
36+
# Linux runners have no display server; start a virtual one before launching the IDE.
37+
- name: Start virtual display (Linux)
38+
if: runner.os == 'Linux'
39+
run: |
40+
sudo apt-get install -y xvfb
41+
Xvfb :99 -screen 0 1920x1080x24 &
42+
sleep 2
43+
echo "DISPLAY=:99" >> "$GITHUB_ENV"
44+
45+
- name: Clean up test module (pre-test)
46+
run: rm -rf src/uiTest/testProject/repository
47+
shell: bash
48+
49+
# Run IDE + tests in a single step so the backgrounded IDE process stays alive.
50+
- name: Start IDE, wait for robot server, and run UI tests
51+
shell: bash
52+
env:
53+
DISPLAY: ${{ env.DISPLAY }}
54+
run: |
55+
echo "Starting IDE with robot server..."
56+
./gradlew runIdeForUiTests > ide-output.log 2>&1 &
57+
IDE_PID=$!
58+
echo "IDE PID: $IDE_PID"
59+
60+
# Poll until the robot server responds to HTTP requests.
61+
echo "Waiting for robot server on port 8082..."
62+
MAX_WAIT=90
63+
ATTEMPT=0
64+
until curl -sf --connect-timeout 2 http://127.0.0.1:8082/api/about > /dev/null 2>&1; do
65+
ATTEMPT=$((ATTEMPT + 1))
66+
if [ "$ATTEMPT" -ge "$MAX_WAIT" ]; then
67+
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
70+
exit 1
71+
fi
72+
echo " attempt $ATTEMPT/$MAX_WAIT — not ready yet, retrying in 5s..."
73+
sleep 5
74+
done
75+
echo "Robot server is ready!"
76+
77+
# Run the UI tests
78+
./gradlew uiTest || TEST_EXIT=$?
79+
80+
echo "=== IDE output (last 50 lines) ==="
81+
tail -50 ide-output.log 2>/dev/null || true
82+
83+
exit ${TEST_EXIT:-0}
84+
85+
- name: Clean up test module (post-test)
86+
if: always()
87+
run: |
88+
rm -rf src/uiTest/testProject/repository
89+
git checkout -- src/uiTest/testProject/settings.gradle.kts
90+
shell: bash
91+
92+
- name: Upload IDE output log
93+
if: always()
94+
uses: actions/upload-artifact@v4
95+
with:
96+
name: ide-output-${{ matrix.os }}
97+
path: ide-output.log
98+
if-no-files-found: ignore
99+
100+
- name: Upload test report
101+
if: always()
102+
uses: actions/upload-artifact@v4
103+
with:
104+
name: ui-test-results-${{ matrix.os }}
105+
path: build/reports/tests/uiTest/
106+
if-no-files-found: ignore
107+
108+
# Upload IDE logs to help diagnose failures
109+
- name: Upload IDE logs on failure
110+
if: failure()
111+
uses: actions/upload-artifact@v4
112+
with:
113+
name: ide-logs-${{ matrix.os }}
114+
path: build/idea-sandbox/system/log/
115+
if-no-files-found: ignore

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ google-services.json
3535
.DS_Store
3636

3737
.intellijPlatform
38+
39+
.kotlin
40+
.claude

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Module Maker Changelog
22

3-
## [1.1.2]
3+
## [1.2.0]
44
- Fix startup issue on Windows
5+
- Change to standard IntelliJ theming
56

67
## [1.1.1]
78
- Platform updates

build.gradle.kts

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ fun environment(key: String) = providers.environmentVariable(key)
77
plugins {
88
id("java") // Java support
99
alias(libs.plugins.kotlin) // Kotlin support
10-
alias(libs.plugins.gradleIntelliJPlugin) // Gradle IntelliJ Plugin
10+
alias(libs.plugins.gradleIntelliJPlugin) // IntelliJ Platform Gradle Plugin
1111
alias(libs.plugins.changelog) // Gradle Changelog Plugin
12-
kotlin("plugin.serialization") version libs.versions.kotlin.get()
13-
id("org.jetbrains.compose")
12+
alias(libs.plugins.compose) // Gradle Compose Compiler Plugin
1413
alias(libs.plugins.spotless)
15-
alias(libs.plugins.compose)
14+
kotlin("plugin.serialization") version libs.versions.kotlin.get()
1615
}
1716

1817
group = properties("pluginGroup").get()
@@ -38,36 +37,36 @@ repositories {
3837
intellijPlatform {
3938
defaultRepositories()
4039
}
40+
maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies")
4141
}
4242

4343
apply(
4444
from = "gradle/spotless.gradle"
4545
)
4646

47+
sourceSets {
48+
create("uiTest") {
49+
kotlin.srcDir("src/uiTest/kotlin")
50+
}
51+
}
52+
4753
dependencies {
4854
implementation(libs.freemarker)
4955
implementation(libs.serialization)
50-
implementation(compose.desktop.currentOs)
51-
implementation(compose.materialIconsExtended)
5256
implementation(libs.segment)
5357

54-
// I usually do
55-
// ./gradlew dependencies | grep "skiko"
56-
// to get the skiko version that compose depends on
57-
val version = "0.9.37.4"
58-
val macTarget = "macos-arm64"
59-
val windowsTarget = "windows-x64"
60-
val linuxTarget = "linux-x64"
61-
62-
implementation("org.jetbrains.skiko:skiko-awt-runtime-$macTarget:$version")
63-
implementation("org.jetbrains.skiko:skiko-awt-runtime-$windowsTarget:$version")
64-
implementation("org.jetbrains.skiko:skiko-awt-runtime-$linuxTarget:$version")
65-
6658
testImplementation(libs.junit)
6759

60+
"uiTestImplementation"(kotlin("stdlib"))
61+
"uiTestImplementation"(libs.remoteRobot)
62+
"uiTestImplementation"(libs.remoteRobotFixtures)
63+
"uiTestImplementation"(libs.junit)
64+
"uiTestImplementation"("com.squareup.okhttp3:okhttp:4.12.0")
65+
// The Compose compiler plugin applies to all source sets; uiTest needs the runtime on its classpath
66+
"uiTestImplementation"("org.jetbrains.compose.runtime:runtime-desktop:1.7.3")
67+
6868
// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
6969
intellijPlatform {
70-
javaCompiler("243.26053.29") // https://github.com/JetBrains/intellij-platform-gradle-plugin/issues/1894
7170
create(properties("platformType").get(), properties("platformVersion").get())
7271

7372
// Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins.
@@ -76,6 +75,10 @@ dependencies {
7675
// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace.
7776
plugins(properties("platformPlugins").map { it.split(',') })
7877

78+
// Compose support dependencies
79+
@Suppress("UnstableApiUsage")
80+
composeUI()
81+
7982
// instrumentationTools()
8083
pluginVerifier()
8184
zipSigner()
@@ -136,6 +139,24 @@ tasks {
136139
}
137140
}
138141

142+
tasks.register<Test>("uiTest") {
143+
description = "Runs UI tests against a running IDE instance"
144+
group = "verification"
145+
testClassesDirs = sourceSets["uiTest"].output.classesDirs
146+
classpath = sourceSets["uiTest"].runtimeClasspath
147+
systemProperty("robot-server.port", System.getProperty("robot-server.port", "8082"))
148+
doNotTrackState("UI tests are not cacheable")
149+
// Gson (used by the remote-robot client) reflectively accesses private fields such as
150+
// Throwable.detailMessage when deserializing error responses from the robot server.
151+
// JDK 17+ JPMS blocks this by default; --add-opens restores access.
152+
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
153+
// Echo test stdout/stderr directly to the Gradle console so diagnostic println calls
154+
// (accessibility tree dumps, screenshots, component class lists) are visible live.
155+
testLogging {
156+
showStandardStreams = true
157+
}
158+
}
159+
139160
intellijPlatformTesting {
140161
runIde {
141162
register("runIdeForUiTests") {
@@ -145,9 +166,24 @@ intellijPlatformTesting {
145166
"-Drobot-server.port=8082",
146167
"-Dide.mac.message.dialogs.as.sheets=false",
147168
"-Djb.privacy.policy.text=<!--999.999-->",
148-
"-Djb.consents.confirmation.enabled=false"
169+
"-Djb.consents.confirmation.enabled=false",
170+
// Skip the "Trust Project?" dialog that blocks the IDE frame from appearing
171+
"-Didea.trust.all.projects=true",
172+
"-Didea.initially.ask.config=never",
173+
// Suppress Tip of the Day, What's New, and other first-run dialogs
174+
"-Dide.show.tips.on.startup.default.value=false",
175+
"-Didea.is.internal=false",
176+
"-Dide.no.platform.update=true",
177+
// Skip import settings dialog
178+
"-Didea.config.imported.in.current.session=true",
179+
// Force the Swing menu bar so remote-robot can find menu items.
180+
// Without this, macOS uses the native system menu bar which is
181+
// invisible to the Swing component hierarchy that remote-robot inspects.
182+
"-Dapple.laf.useScreenMenuBar=false"
149183
)
150184
}
185+
// Open the test project so settings.gradle.kts is available for module creation
186+
args(layout.projectDirectory.dir("src/uiTest/testProject").asFile.absolutePath)
151187
}
152188

153189
plugins {

gradle.properties

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pluginGroup = com.joetr.modulemaker
44
pluginName = ModuleMaker
55
pluginRepositoryUrl = https://github.com/j-roskopf/ModuleMakerPlugin
66
# SemVer format -> https://semver.org
7-
pluginVersion = 1.1.2
7+
pluginVersion = 1.2.0
88

99
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
1010
pluginSinceBuild = 222
@@ -39,4 +39,3 @@ org.gradle.caching = true
3939
# Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment
4040
systemProp.org.gradle.unsafe.kotlin.assignment = true
4141

42-
compose.version=1.10.1

gradle/libs.versions.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
freemarker = "2.3.30"
33
serialization = "1.5.1"
44
jdk = "17"
5-
kotlin = "2.3.0"
5+
kotlin = "2.1.20"
66
changelog = "2.0.0"
7-
gradleIntelliJPlugin = "2.11.0"
7+
gradleIntelliJPlugin = "2.10.5"
88
spotless = "6.8.0"
99
segment = "1.13.2"
1010
junit = "4.13.2"
11+
remoteRobot = "0.11.23"
1112

1213
[libraries]
1314
freemarker = { group = "org.freemarker", name = "freemarker", version.ref = "freemarker" }
1415
serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
1516
segment = { group = "com.segment.analytics.kotlin", name = "core", version.ref = "segment" }
1617
junit = { group = "junit", name = "junit", version.ref = "junit" }
18+
remoteRobot = { group = "com.intellij.remoterobot", name = "remote-robot", version.ref = "remoteRobot" }
19+
remoteRobotFixtures = { group = "com.intellij.remoterobot", name = "remote-fixtures", version.ref = "remoteRobot" }
1720

1821
[plugins]
1922
changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }

run-ui-tests.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Start the IDE with the robot server in the background.
5+
# runIdeForUiTests never exits on its own, so we launch it as a background process
6+
# and kill it when the tests finish.
7+
echo "Starting IDE with robot server..."
8+
./gradlew runIdeForUiTests &
9+
IDE_PID=$!
10+
11+
cleanup() {
12+
echo "Stopping IDE (pid $IDE_PID)..."
13+
kill "$IDE_PID" 2>/dev/null || true
14+
}
15+
trap cleanup EXIT
16+
17+
# Wait for the robot server TCP port to accept connections (up to 5 minutes).
18+
# The server has no GET health endpoint; nc is the reliable way to probe the port.
19+
echo "Waiting for robot server on port 8082..."
20+
MAX_ATTEMPTS=60
21+
ATTEMPT=0
22+
until nc -z localhost 8082 2>/dev/null; do
23+
ATTEMPT=$((ATTEMPT + 1))
24+
if [ "$ATTEMPT" -ge "$MAX_ATTEMPTS" ]; then
25+
echo "ERROR: robot server did not start after $((MAX_ATTEMPTS * 5)) seconds."
26+
exit 1
27+
fi
28+
echo " attempt $ATTEMPT/$MAX_ATTEMPTS — not ready yet, retrying in 5s..."
29+
sleep 5
30+
done
31+
echo "IDE is ready!"
32+
33+
# Clean up any previously created test module before running.
34+
TEST_MODULE_DIR="src/uiTest/testProject/repository"
35+
rm -rf "$TEST_MODULE_DIR"
36+
37+
# Run the tests. The EXIT trap above will kill the IDE when this exits.
38+
./gradlew uiTest
39+
TEST_EXIT=$?
40+
41+
# Clean up the created test module and restore settings.gradle.kts after running.
42+
rm -rf "$TEST_MODULE_DIR"
43+
git checkout -- src/uiTest/testProject/settings.gradle.kts
44+
45+
exit $TEST_EXIT

settings.gradle.kts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
11
rootProject.name = "Module Maker"
2-
3-
pluginManagement {
4-
plugins {
5-
id("org.jetbrains.compose").version(extra["compose.version"] as String)
6-
}
7-
}

0 commit comments

Comments
 (0)