Skip to content

Commit f8478fb

Browse files
committed
feat(tests): integration tests framework
1 parent 7ac809b commit f8478fb

22 files changed

Lines changed: 822 additions & 3 deletions

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ We are standardizing only the minimum plugin shape needed to support:
77
- a Godot-facing Kirie service
88
- a scene-friendly KirieView node
99
- Android and iOS native WebView implementations
10-
- an example project used for integration testing
10+
- a repo-level platform integration test project
1111

1212
Anything beyond that, such as dedicated protocol packages, CLI tooling, or
1313
adapters, is deferred until the IPC model is proven.

docs/platform-integration-tests.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Platform Integration Tests
2+
3+
Kirie platform integration tests live in a repo-level Godot project:
4+
5+
```text
6+
tests/integration/
7+
```
8+
9+
They are not Android instrumentation tests and are not part of
10+
`examples/basic-ipc`. Android and iOS should eventually run the same Godot test
11+
project; the platform only provides the WebView runtime and app launch
12+
mechanism.
13+
14+
## Goals
15+
16+
These tests cover the platform bridge path:
17+
18+
```text
19+
Godot -> Kirie platform singleton -> platform WebView -> JavaScript -> Godot
20+
```
21+
22+
The current focus is:
23+
24+
- WebView lifecycle behavior from Godot
25+
- raw JavaScript bridge IPC
26+
- resource loading through `res://`
27+
- exported app behavior, not editor-only behavior
28+
29+
The tests intentionally do not depend on the browser-facing `@kirie/ipc`
30+
package. That package is a convenience SDK above the raw bridge contract and
31+
should be tested separately.
32+
33+
## Project Layout
34+
35+
```text
36+
tests/integration/
37+
project.godot
38+
export_presets.cfg
39+
main.tscn
40+
addons/kirie -> ../../../packages/kirie/addon/addons/kirie
41+
scripts/
42+
test_runner.gd
43+
test_probe.gd
44+
test_cases/
45+
ipc_round_trip_probe.gd
46+
webview_lifecycle_probe.gd
47+
res_asset_loading_probe.gd
48+
web/
49+
probe.html
50+
```
51+
52+
`web/probe.html` is a minimal fixture, not a web app project. There is no Vite
53+
project, package.json, TypeScript config, or generated web bundle under
54+
`tests/integration/web`.
55+
56+
## Runner Contract
57+
58+
The exported app runs one test per app session.
59+
60+
On Android, the runner reads the test name from the launch option:
61+
62+
```text
63+
kirie_test
64+
```
65+
66+
The Android plugin exposes this through:
67+
68+
```gdscript
69+
Kirie.get_launch_option("kirie_test")
70+
```
71+
72+
The runner also supports `--kirie-test=<name>` from Godot command-line user
73+
args for local non-Android runs.
74+
75+
`scripts/test_runner.gd` owns only:
76+
77+
- resolving the test name
78+
- loading `res://scripts/test_cases/<test_name>.gd`
79+
- calling `run(kirie, tree, test_name)`
80+
- printing pass/fail markers
81+
- quitting the app
82+
83+
Test cases return a `String`:
84+
85+
- `""` means pass
86+
- non-empty string means fail reason
87+
88+
The runner prints exactly one final marker:
89+
90+
```text
91+
KIRIE_TEST_PASS <test_name>
92+
KIRIE_TEST_FAIL <test_name> <reason>
93+
```
94+
95+
## Test Case Contract
96+
97+
Each test case owns its own lifecycle operations. A test should explicitly call
98+
the Kirie API it wants to exercise:
99+
100+
- `create_webview()`
101+
- `load_html_string(...)`
102+
- `load_url(...)`
103+
- `send_ipc_message(...)`
104+
- `destroy_webview()`
105+
106+
Shared waiting and probe observation lives in `scripts/test_probe.gd`.
107+
`KirieIntegrationProbe` may:
108+
109+
- connect to Kirie signals
110+
- wait for `webview_ready`
111+
- collect `ipc_message_received`
112+
- wait for a specific probe message
113+
- read `web/probe.html`
114+
115+
It should not decide which URL a test loads. Page URLs are test inputs and must
116+
be provided by the test case itself.
117+
118+
## Web Fixture
119+
120+
`web/probe.html` is a small raw bridge fixture. It uses the platform-level
121+
contract directly:
122+
123+
- Android JavaScript to Godot:
124+
`globalThis.KirieAndroidBridge.postMessage(messageJson)`
125+
- iOS JavaScript to Godot:
126+
`globalThis.webkit.messageHandlers.kirie.postMessage(messageJson)`
127+
- Godot to JavaScript:
128+
`kirie:ipc-message` DOM events
129+
130+
For tests that do not care about asset loading, the GDScript case reads
131+
`probe.html` and injects it with `load_html_string(...)`. This keeps the test
132+
case in Godot while preserving HTML/JavaScript syntax highlighting in the
133+
fixture file.
134+
135+
For tests that do care about exported resources, the case loads
136+
`res://web/probe.html?...` with `load_url(...)`.
137+
138+
## Test Coverage Shape
139+
140+
Individual test behavior belongs in `scripts/test_cases/*.gd`, not in this
141+
architecture note.
142+
143+
The suite should stay organized around a small number of platform-facing
144+
coverage categories:
145+
146+
- IPC round trips through the raw JavaScript bridge
147+
- WebView lifecycle transitions driven from Godot
148+
- exported `res://` web resource loading
149+
150+
New tests should add a focused case under `scripts/test_cases/` when they need
151+
different lifecycle operations, a different loaded URL, or a different platform
152+
bridge assertion.
153+
154+
## Android Local Flow
155+
156+
Build the test APK:
157+
158+
```bash
159+
scripts/build_integration_android.sh
160+
```
161+
162+
Install it once:
163+
164+
```bash
165+
adb install -r dist/integration/android_debug.apk
166+
```
167+
168+
Run one test:
169+
170+
```bash
171+
scripts/run_integration_android_test.sh ipc_round_trip_probe
172+
```
173+
174+
The run script:
175+
176+
- clears logcat
177+
- force-stops the package
178+
- clears app data
179+
- starts the exported app with `--es kirie_test <test_name>`
180+
- waits for `KIRIE_TEST_PASS` or `KIRIE_TEST_FAIL`
181+
182+
The Android package defaults to:
183+
184+
```text
185+
ai.moeru.kirie.integrationtests
186+
```
187+
188+
The Android launcher component defaults to:
189+
190+
```text
191+
com.godot.game.GodotAppLauncher
192+
```
193+
194+
## Isolation Model
195+
196+
Tests are isolated by app session:
197+
198+
- export one test APK
199+
- install it once
200+
- run each test in a fresh app start
201+
- run `pm clear` before each test
202+
203+
This avoids residual WebView, JavaScript, singleton, signal, and cache state
204+
without exporting a separate APK for every test.
205+
206+
## CI Direction
207+
208+
The intended GitHub Actions shape is:
209+
210+
- set up Node and Java with `jdx/mise-action`
211+
- set up Godot and export templates with `chickensoft-games/setup-godot`
212+
- install pnpm dependencies
213+
- build the Android AAR
214+
- export `tests/integration` as an Android APK
215+
- start an emulator with `reactivecircus/android-emulator-runner`
216+
- install the APK once
217+
- run each test through `scripts/run_integration_android_test.sh`
218+
219+
CI should reuse the same marker contract and app-session isolation used
220+
locally.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"build": "pnpm -r run build",
77
"format": "pnpm run format:biome && pnpm run format:gdscript && pnpm run format:kotlin && pnpm run format:swift",
88
"format:biome": "biome check --write .",
9-
"format:gdscript": "gdformat packages/kirie/addon/addons/kirie examples/basic-ipc/scripts",
9+
"format:gdscript": "gdformat packages/kirie/addon/addons/kirie examples/basic-ipc/scripts tests/integration/scripts",
1010
"format:kotlin": "ktlint --format \"packages/kirie/native/android/**/*.kt\" \"packages/kirie/native/android/**/*.kts\"",
1111
"format:swift": "swiftlint lint --fix --format --no-cache --config .swiftlint.yml",
1212
"lint": "pnpm run lint:biome && pnpm run lint:gdscript && pnpm run lint:kotlin && pnpm run lint:swift",
1313
"lint:biome": "biome check .",
14-
"lint:gdscript": "gdlint packages/kirie/addon/addons/kirie examples/basic-ipc/scripts",
14+
"lint:gdscript": "gdlint packages/kirie/addon/addons/kirie examples/basic-ipc/scripts tests/integration/scripts",
1515
"lint:kotlin": "ktlint \"packages/kirie/native/android/**/*.kt\" \"packages/kirie/native/android/**/*.kts\"",
1616
"lint:swift": "swiftlint lint --strict --no-cache --config .swiftlint.yml",
1717
"typecheck": "pnpm -r run typecheck"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
if [ ! -f "tests/integration/project.godot" ]; then
6+
echo "This script must be run from the repository root." >&2
7+
exit 1
8+
fi
9+
10+
APK_PATH="${APK_PATH:-dist/integration/android_debug.apk}"
11+
12+
mkdir -p "$(dirname "${APK_PATH}")"
13+
14+
mise x -- packages/kirie/native/android/gradlew \
15+
--project-dir packages/kirie/native/android \
16+
:plugin:assembleDebug
17+
18+
if [ -n "${GODOT_BIN:-}" ]; then
19+
godot_command=("${GODOT_BIN}")
20+
else
21+
godot_path="$(mise which godot)"
22+
resolved_godot_path="$(readlink "${godot_path}" || true)"
23+
if [ -n "${resolved_godot_path}" ] && [ -x "${resolved_godot_path}" ]; then
24+
godot_command=(mise x -- "${resolved_godot_path}")
25+
else
26+
godot_command=(mise x -- "${godot_path}")
27+
fi
28+
fi
29+
30+
"${godot_command[@]}" \
31+
--headless \
32+
--path tests/integration \
33+
--install-android-build-template \
34+
--export-debug "Android" \
35+
"../../${APK_PATH}"
36+
37+
echo "Exported ${APK_PATH}"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
if [ ! -f "tests/integration/project.godot" ]; then
6+
echo "This script must be run from the repository root." >&2
7+
exit 1
8+
fi
9+
10+
if [ "$#" -ne 1 ]; then
11+
echo "Usage: scripts/run_integration_android_test.sh <test_name>" >&2
12+
exit 1
13+
fi
14+
15+
TEST_NAME="$1"
16+
PACKAGE_NAME="${PACKAGE_NAME:-ai.moeru.kirie.integrationtests}"
17+
ACTIVITY_NAME="${ACTIVITY_NAME:-com.godot.game.GodotAppLauncher}"
18+
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-30}"
19+
LOG_FILE="${LOG_FILE:-${TMPDIR:-/tmp}/kirie-integration-${TEST_NAME}.log}"
20+
21+
cleanup() {
22+
if [ -n "${LOGCAT_PID:-}" ]; then
23+
kill "${LOGCAT_PID}" >/dev/null 2>&1 || true
24+
wait "${LOGCAT_PID}" >/dev/null 2>&1 || true
25+
fi
26+
}
27+
trap cleanup EXIT
28+
29+
: > "${LOG_FILE}"
30+
adb logcat -c
31+
adb shell am force-stop "${PACKAGE_NAME}" >/dev/null 2>&1 || true
32+
adb shell pm clear "${PACKAGE_NAME}" >/dev/null
33+
34+
adb logcat > "${LOG_FILE}" &
35+
LOGCAT_PID="$!"
36+
37+
adb shell am start \
38+
-n "${PACKAGE_NAME}/${ACTIVITY_NAME}" \
39+
--es kirie_test "${TEST_NAME}" \
40+
>/dev/null
41+
42+
deadline=$((SECONDS + TIMEOUT_SECONDS))
43+
while [ "${SECONDS}" -lt "${deadline}" ]; do
44+
if fail_line="$(grep -m 1 -E "KIRIE_TEST_FAIL (${TEST_NAME}|unknown)( |$)" "${LOG_FILE}")"; then
45+
echo "${fail_line}" >&2
46+
exit 1
47+
fi
48+
49+
if pass_line="$(grep -m 1 -E "KIRIE_TEST_PASS ${TEST_NAME}( |$)" "${LOG_FILE}")"; then
50+
echo "${pass_line}"
51+
exit 0
52+
fi
53+
54+
sleep 0.5
55+
done
56+
57+
echo "Timed out waiting for KIRIE_TEST_PASS or KIRIE_TEST_FAIL for ${TEST_NAME}" >&2
58+
tail -n 120 "${LOG_FILE}" >&2
59+
exit 1

tests/integration/addons/kirie

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../packages/kirie/addon/addons/kirie
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[preset.0]
2+
3+
name="Android"
4+
platform="Android"
5+
runnable=true
6+
dedicated_server=false
7+
custom_features=""
8+
export_filter="all_resources"
9+
include_filter="web/probe.html"
10+
exclude_filter=""
11+
export_path=""
12+
encryption_include_filters=""
13+
encryption_exclude_filters=""
14+
encrypt_pck=false
15+
encrypt_directory=false
16+
script_export_mode=2
17+
18+
[preset.0.options]
19+
20+
gradle_build/use_gradle_build=true
21+
gradle_build/export_format=0
22+
architectures/armeabi-v7a=false
23+
architectures/arm64-v8a=true
24+
architectures/x86=false
25+
architectures/x86_64=false
26+
package/unique_name="ai.moeru.kirie.integrationtests"
27+
package/signed=true
28+
package/show_as_launcher_app=false
29+
screen/immersive_mode=true
30+
screen/support_small=true
31+
screen/support_normal=true
32+
screen/support_large=true
33+
screen/support_xlarge=true

tests/integration/icon.svg

Lines changed: 5 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)