diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a8f24d..513ba62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,10 @@ on: pull_request: branches: [main] # Skip CI when a PR only touches documentation / media / repo metadata. - # Spotless, JVM tests, WASM compile, and iOS compile all only care about - # actual source code and build configuration. paths-ignore is matched - # against the entire PR diff — a PR that edits README AND a .kt file - # still runs CI, because the .kt file is not in this list. + # Spotless and the per-platform test jobs all only care about actual source + # code and build configuration. paths-ignore is matched against the entire + # PR diff — a PR that edits README AND a .kt file still runs CI, because the + # .kt file is not in this list. paths-ignore: - '**/*.md' - 'docs/**' @@ -21,8 +21,12 @@ concurrency: cancel-in-progress: true jobs: - validate: - name: Validate (JVM, Lint, WASM) + # Lint + JVM/JS/WASM tests. Every target below runs the shared commonTest + # suite (which includes the end-to-end "user can draw a stroke" tests), so a + # green run proves drawing actually works on that platform — not just that it + # compiles. + test: + name: Test (JVM, JS, WASM) + Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -32,14 +36,30 @@ jobs: distribution: temurin java-version: 21 + # JS and WASM browser tests run under Karma against headless Chrome, so a + # real browser must be on PATH for the runner to launch it. Export its + # path as CHROME_BIN, which karma-chrome-launcher uses to locate Chrome. + - name: Setup Chrome + id: setup-chrome + uses: browser-actions/setup-chrome@v1 + + - name: Expose Chrome to Karma + run: echo "CHROME_BIN=${{ steps.setup-chrome.outputs.chrome-path }}" >> "$GITHUB_ENV" + - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - name: Spotless check - run: ./gradlew :DrawBox:spotlessCheck + run: ./gradlew :DrawBox:spotlessCheck :drawbox-ui:spotlessCheck - name: JVM tests - run: ./gradlew :DrawBox:jvmTest + run: ./gradlew :DrawBox:jvmTest :drawbox-ui:jvmTest + + - name: JS (browser) tests + run: ./gradlew :DrawBox:jsTest :drawbox-ui:jsTest + + - name: WASM (browser) tests + run: ./gradlew :DrawBox:wasmJsTest :drawbox-ui:wasmJsTest - name: WASM sample build run: ./gradlew :webApp:wasmJsBrowserDistribution @@ -48,12 +68,16 @@ jobs: if: failure() uses: actions/upload-artifact@v7 with: - name: jvm-test-reports - path: DrawBox/build/reports/tests/jvmTest + name: multiplatform-test-reports + path: | + DrawBox/build/reports/tests + drawbox-ui/build/reports/tests if-no-files-found: ignore - ios-compile: - name: iOS compile check + # iOS runs the same commonTest suite on the Kotlin/Native simulator target, + # so drawing is verified on Apple platforms too — not just compiled. + ios-test: + name: iOS test (simulator) runs-on: macos-latest steps: - uses: actions/checkout@v7 @@ -66,5 +90,15 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - - name: Compile iosSimulatorArm64 - run: ./gradlew :DrawBox:compileKotlinIosSimulatorArm64 \ No newline at end of file + - name: iOS simulator tests + run: ./gradlew :DrawBox:iosSimulatorArm64Test :drawbox-ui:iosSimulatorArm64Test + + - name: Upload test reports + if: failure() + uses: actions/upload-artifact@v7 + with: + name: ios-test-reports + path: | + DrawBox/build/reports/tests + drawbox-ui/build/reports/tests + if-no-files-found: ignore \ No newline at end of file diff --git a/DrawBox/karma.config.d/no-sandbox.js b/DrawBox/karma.config.d/no-sandbox.js new file mode 100644 index 0000000..1f957c8 --- /dev/null +++ b/DrawBox/karma.config.d/no-sandbox.js @@ -0,0 +1,15 @@ +// Kotlin/JS and Kotlin/WASM browser tests run under Karma against headless +// Chrome. On Linux CI runners Chrome's setuid sandbox cannot initialize +// (ZygoteHostImpl fatal -> "cannot start"), so launch with --no-sandbox. +// Harmless on local macOS/Windows runs. The Kotlin Gradle plugin concatenates +// every file in this directory into the generated Karma config, where `config` +// is in scope. +config.set({ + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, +}); \ No newline at end of file diff --git a/DrawBox/src/commonTest/kotlin/io/ak1/drawbox/presentation/viewmodel/DrawBoxControllerTest.kt b/DrawBox/src/commonTest/kotlin/io/ak1/drawbox/presentation/viewmodel/DrawBoxControllerTest.kt index ce7dadb..b18290b 100644 --- a/DrawBox/src/commonTest/kotlin/io/ak1/drawbox/presentation/viewmodel/DrawBoxControllerTest.kt +++ b/DrawBox/src/commonTest/kotlin/io/ak1/drawbox/presentation/viewmodel/DrawBoxControllerTest.kt @@ -1,7 +1,10 @@ package io.ak1.drawbox.presentation.viewmodel +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import io.ak1.drawbox.domain.model.Element import io.ak1.drawbox.domain.model.Intent +import io.ak1.drawbox.domain.model.Mode import io.ak1.drawbox.domain.usecase.UseCase import io.ak1.drawbox.presentation.reducer.Reducer import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -12,6 +15,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) @@ -19,6 +23,48 @@ class DrawBoxControllerTest { private fun newController() = DrawBoxController(Reducer(UseCase())) + // ===== End-to-end "user draws a stroke" ===== + // + // These drive the same intent sequence the DrawBox gesture layer dispatches + // when a user presses, drags, and lifts in PEN mode. They live in commonTest + // so every published target — JVM, JS, WASM, iOS — proves a user can actually + // draw, not merely that the code compiles. + + @Test + fun userCanDrawAFreehandStroke() { + val controller = newController() + controller.setMode(Mode.PEN) + + // Press down, then drag through a few points and lift. + controller.onIntent(Intent.InsertNewPath(Offset(10f, 10f))) + controller.onIntent(Intent.UpdateLatestPath(Offset(20f, 15f))) + controller.onIntent(Intent.UpdateLatestPath(Offset(30f, 25f))) + controller.onIntent(Intent.UpdateLatestPath(Offset(40f, 40f))) + + val elements = controller.state.value.elements + assertEquals(1, elements.size, "the drag should have produced exactly one stroke") + val path = elements[0] as Element.Path + assertEquals(4, path.samples.size, "seed point plus three drag samples") + assertEquals(Offset(10f, 10f), path.samples.first().position) + assertEquals(Offset(40f, 40f), path.samples.last().position) + assertTrue(controller.canUndo.value, "a finished stroke must be undoable") + } + + @Test + fun undoRemovesADrawnStroke() { + val controller = newController() + controller.setMode(Mode.PEN) + controller.onIntent(Intent.InsertNewPath(Offset(0f, 0f))) + controller.onIntent(Intent.UpdateLatestPath(Offset(50f, 50f))) + assertEquals(1, controller.state.value.elements.size) + + controller.undo() + + assertTrue(controller.state.value.elements.isEmpty(), "undo should clear the stroke") + assertFalse(controller.canUndo.value) + assertTrue(controller.canRedo.value, "the undone stroke should be redoable") + } + @Test fun intentsFlowEmitsEveryProcessedIntent() = runTest(StandardTestDispatcher()) { val controller = newController() diff --git a/drawbox-ui/karma.config.d/no-sandbox.js b/drawbox-ui/karma.config.d/no-sandbox.js new file mode 100644 index 0000000..1f957c8 --- /dev/null +++ b/drawbox-ui/karma.config.d/no-sandbox.js @@ -0,0 +1,15 @@ +// Kotlin/JS and Kotlin/WASM browser tests run under Karma against headless +// Chrome. On Linux CI runners Chrome's setuid sandbox cannot initialize +// (ZygoteHostImpl fatal -> "cannot start"), so launch with --no-sandbox. +// Harmless on local macOS/Windows runs. The Kotlin Gradle plugin concatenates +// every file in this directory into the generated Karma config, where `config` +// is in scope. +config.set({ + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, +}); \ No newline at end of file