mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
test: migrate Compose UI tests from androidTest to commonTest (#5091)
This commit is contained in:
parent
4156acf297
commit
a11dee42a7
27 changed files with 296 additions and 597 deletions
108
.github/workflows/codeql.yml
vendored
108
.github/workflows/codeql.yml
vendored
|
|
@ -1,108 +0,0 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches: [ "main" ]
|
||||
# pull_request:
|
||||
# branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-24.04' }}
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: java-kotlin
|
||||
build-mode: autobuild
|
||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
# or others). This is typically only required for manual builds.
|
||||
# - name: Setup runtime (example)
|
||||
# uses: actions/setup-example@v1
|
||||
- name: Java Setup
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '21'
|
||||
token: ${{ github.token }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
2
.github/workflows/main-check.yml
vendored
2
.github/workflows/main-check.yml
vendored
|
|
@ -21,8 +21,6 @@ jobs:
|
|||
with:
|
||||
run_lint: true
|
||||
run_unit_tests: false
|
||||
run_instrumented_tests: false
|
||||
run_desktop_builds: false
|
||||
api_levels: '[35]'
|
||||
upload_artifacts: true
|
||||
secrets: inherit
|
||||
|
|
|
|||
2
.github/workflows/merge-queue.yml
vendored
2
.github/workflows/merge-queue.yml
vendored
|
|
@ -18,8 +18,6 @@ jobs:
|
|||
with:
|
||||
run_lint: true
|
||||
run_unit_tests: true
|
||||
run_instrumented_tests: true
|
||||
api_levels: '[26, 35]' # Comprehensive testing for Merge Queue
|
||||
upload_artifacts: false
|
||||
secrets: inherit
|
||||
|
||||
|
|
|
|||
8
.github/workflows/pull-request.yml
vendored
8
.github/workflows/pull-request.yml
vendored
|
|
@ -99,9 +99,9 @@ jobs:
|
|||
PY
|
||||
|
||||
# 2. VALIDATION & BUILD: Delegate to reusable-check.yml
|
||||
# We disable instrumented tests, coverage, and desktop builds for PRs to keep
|
||||
# feedback fast (< 10 mins). Desktop compilation is already covered by the
|
||||
# :desktop:test task in the shard-app test shard.
|
||||
# We disable coverage and desktop builds for PRs to keep feedback fast
|
||||
# (< 10 mins). Desktop compilation is already covered by the :desktop:test
|
||||
# task in the shard-app test shard.
|
||||
validate-and-build:
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.android == 'true'
|
||||
|
|
@ -109,10 +109,8 @@ jobs:
|
|||
with:
|
||||
run_lint: true
|
||||
run_unit_tests: true
|
||||
run_instrumented_tests: false
|
||||
run_coverage: false
|
||||
run_desktop_builds: false
|
||||
api_levels: '[35]'
|
||||
upload_artifacts: true
|
||||
secrets: inherit
|
||||
|
||||
|
|
|
|||
96
.github/workflows/reusable-check.yml
vendored
96
.github/workflows/reusable-check.yml
vendored
|
|
@ -9,15 +9,9 @@ on:
|
|||
run_unit_tests:
|
||||
type: boolean
|
||||
default: true
|
||||
run_instrumented_tests:
|
||||
type: boolean
|
||||
default: true
|
||||
run_coverage:
|
||||
type: boolean
|
||||
default: true
|
||||
api_levels:
|
||||
type: string
|
||||
default: '[35]'
|
||||
run_desktop_builds:
|
||||
type: boolean
|
||||
default: true
|
||||
|
|
@ -238,7 +232,7 @@ jobs:
|
|||
**/build/test-results
|
||||
retention-days: 7
|
||||
|
||||
# ── Android Build & Instrumented Tests ──────────────────────────────
|
||||
# ── Android Build ────────────────────────────────────────────────────
|
||||
android-check:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
|
|
@ -247,10 +241,6 @@ jobs:
|
|||
needs: lint-check
|
||||
env:
|
||||
VERSION_CODE: ${{ needs.lint-check.outputs.version_code }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
api_level: ${{ fromJson(inputs.api_levels) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
@ -265,99 +255,25 @@ jobs:
|
|||
gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }}
|
||||
|
||||
- name: Determine matrix metadata
|
||||
id: matrix_meta
|
||||
shell: bash
|
||||
run: |
|
||||
first_api=$(python3 - <<'PY'
|
||||
import json
|
||||
print(json.loads('${{ inputs.api_levels }}')[0])
|
||||
PY
|
||||
)
|
||||
|
||||
if [[ "${{ matrix.api_level }}" == "$first_api" ]]; then
|
||||
echo "is_first_api=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_first_api=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Determine Android tasks
|
||||
id: tasks
|
||||
shell: bash
|
||||
run: |
|
||||
tasks=(
|
||||
"app:assembleFdroidDebug"
|
||||
"app:assembleGoogleDebug"
|
||||
)
|
||||
|
||||
if [[ "${{ inputs.run_instrumented_tests }}" == "true" ]]; then
|
||||
tasks+=(
|
||||
"app:connectedFdroidDebugAndroidTest"
|
||||
"app:connectedGoogleDebugAndroidTest"
|
||||
)
|
||||
fi
|
||||
|
||||
printf 'tasks=%s\n' "${tasks[*]}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Enable KVM group perms
|
||||
if: inputs.run_instrumented_tests == true
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Run Android Build & Instrumented Tests
|
||||
if: inputs.run_instrumented_tests == true
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api_level }}
|
||||
arch: x86_64
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true --parallel --configuration-cache --continue --scan
|
||||
|
||||
- name: Run Android Build
|
||||
if: inputs.run_instrumented_tests == false
|
||||
run: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true --parallel --configuration-cache --continue --scan
|
||||
|
||||
- name: Upload instrumented test results to Codecov
|
||||
if: ${{ !cancelled() && inputs.run_instrumented_tests && steps.matrix_meta.outputs.is_first_api == 'true' }}
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: meshtastic/Meshtastic-Android
|
||||
flags: android-instrumented
|
||||
fail_ci_if_error: false
|
||||
report_type: test_results
|
||||
files: "**/build/outputs/androidTest-results/**/*.xml"
|
||||
- name: Build Android APKs
|
||||
run: ./gradlew app:assembleFdroidDebug app:assembleGoogleDebug -Pci=true --parallel --configuration-cache --continue --scan
|
||||
|
||||
- name: Upload debug artifact
|
||||
if: ${{ steps.matrix_meta.outputs.is_first_api == 'true' && inputs.upload_artifacts }}
|
||||
if: ${{ inputs.upload_artifacts }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: app-debug-apks
|
||||
path: app/build/outputs/apk/*/debug/*.apk
|
||||
retention-days: 14
|
||||
retention-days: 7
|
||||
|
||||
- name: Report App Size
|
||||
if: ${{ always() && steps.matrix_meta.outputs.is_first_api == 'true' }}
|
||||
if: always()
|
||||
run: |
|
||||
echo "### App Size Report" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY
|
||||
find app/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload Android reports
|
||||
if: ${{ always() && inputs.upload_artifacts }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: reports-android-api-${{ matrix.api_level }}
|
||||
path: |
|
||||
**/build/outputs/androidTest-results
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
# ── Desktop Build ───────────────────────────────────────────────────
|
||||
build-desktop:
|
||||
name: Build Desktop Debug (${{ matrix.os }})
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ When reviewing code, meticulously verify the following categories. Flag any devi
|
|||
- [ ] **Compose Multiplatform:** Ensure `compose-multiplatform-*` aliases are used instead of plain `androidx.compose` in all KMP modules.
|
||||
|
||||
### 7. Testing
|
||||
- [ ] **Test Placement:** New Compose UI tests must go in `commonTest` using `runComposeUiTest {}` + `kotlin.test.Test`. Do not add `androidTest` (instrumented) tests.
|
||||
- [ ] **Shared Test Utilities:** Test fakes, doubles, and utilities should be placed in `core:testing`.
|
||||
- [ ] **Libraries:** Verify usage of `Turbine` for Flow testing, `Kotest` for property-based testing, and `Mokkery` for mocking.
|
||||
- [ ] **Robolectric Configuration:** Check that Compose UI tests running via Robolectric on JVM are pinned to `@Config(sdk = [34])` to prevent Java 21 / SDK 35 compatibility issues.
|
||||
|
|
|
|||
|
|
@ -34,14 +34,13 @@ Run in this order for routine changes to ensure code formatting, analysis, and b
|
|||
- `worker/service/background` changes: Broad tests, targeted WorkManager checks.
|
||||
- `BLE/networking/core repository`: `spotlessCheck`, `detekt`, `assembleDebug`, `test allTests`.
|
||||
|
||||
## 3) Flavor and instrumentation checks
|
||||
## 3) Flavor checks
|
||||
|
||||
Run these when relevant to map, provider, or flavor-specific behavior:
|
||||
|
||||
```bash
|
||||
./gradlew lintFdroidDebug lintGoogleDebug
|
||||
./gradlew testFdroidDebug testGoogleDebug
|
||||
./gradlew connectedAndroidTest
|
||||
```
|
||||
|
||||
## 4) CI Pipeline Architecture
|
||||
|
|
@ -55,12 +54,12 @@ CI is defined in `.github/workflows/reusable-check.yml` and structured as four p
|
|||
- `shard-app`: Explicit test tasks for pure-Android/JVM modules (`app`, `desktop`, `core:barcode`, `mesh_service_example`).
|
||||
Each shard generates Kover XML coverage and uploads test results + coverage to Codecov with per-shard flags.
|
||||
Downstream jobs use `fetch-depth: 1` and receive `VERSION_CODE` from lint-check via env var, enabling shallow clones.
|
||||
3. **`android-check`** — Builds APKs and runs instrumented tests (depends on `lint-check`).
|
||||
3. **`android-check`** — Builds APKs for all flavors (depends on `lint-check`).
|
||||
4. **`build-desktop`** — Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) that builds desktop distributions via `createDistributable` (depends on `lint-check`).
|
||||
|
||||
### Runner Strategy (Three Tiers)
|
||||
- **`ubuntu-24.04-arm`** — Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). Benefits from ARM runners' shorter queue times.
|
||||
- **`ubuntu-24.04`** — Main Gradle-heavy jobs (CI `lint-check`/`test-shards`/`android-check`, release builds, Dokka, CodeQL, publish, dependency-submission). Pin for reproducibility.
|
||||
- **`ubuntu-24.04`** — Main Gradle-heavy jobs (CI `lint-check`/`test-shards`/`android-check`, release builds, Dokka, publish, dependency-submission). Pin for reproducibility.
|
||||
- **Desktop runners:** Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) for the `build-desktop` job and release packaging.
|
||||
|
||||
### CI Gradle Properties
|
||||
|
|
|
|||
|
|
@ -297,12 +297,6 @@ dependencies {
|
|||
fdroidImplementation(libs.osmdroid.geopackage) { exclude(group = "com.j256.ormlite") }
|
||||
fdroidImplementation(libs.osmbonuspack)
|
||||
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.kotlinx.coroutines.test)
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.koin.test)
|
||||
|
||||
testImplementation(kotlin("test-junit"))
|
||||
testImplementation(libs.androidx.work.testing)
|
||||
testImplementation(libs.koin.test)
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.filter
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.inject
|
||||
import org.meshtastic.core.repository.FilterPrefs
|
||||
import org.meshtastic.core.repository.MessageFilter
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageFilterIntegrationTest : KoinTest {
|
||||
|
||||
private val filterPrefs: FilterPrefs by inject()
|
||||
|
||||
private val filterService: MessageFilter by inject()
|
||||
|
||||
@org.junit.Ignore("Flaky integration test, needs Koin test rule setup")
|
||||
@Test
|
||||
fun filterPrefsIntegration() = runTest {
|
||||
filterPrefs.setFilterEnabled(true)
|
||||
filterPrefs.setFilterWords(setOf("test", "spam"))
|
||||
// Wait briefly for DataStore to process the writes and flows to emit
|
||||
kotlinx.coroutines.delay(100)
|
||||
filterService.rebuildPatterns()
|
||||
|
||||
assertTrue(filterService.shouldFilter("this is a test message"))
|
||||
assertTrue(filterService.shouldFilter("spam content"))
|
||||
}
|
||||
}
|
||||
|
|
@ -50,12 +50,5 @@ kotlin {
|
|||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(projects.core.testing)
|
||||
}
|
||||
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.androidx.lifecycle.testing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ kotlin {
|
|||
android {
|
||||
namespace = "org.meshtastic.core.ui"
|
||||
androidResources.enable = false
|
||||
withHostTest { isIncludeAndroidResources = true }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
|
@ -70,8 +69,9 @@ kotlin {
|
|||
implementation(projects.core.testing)
|
||||
implementation(libs.junit)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.compose.multiplatform.ui.test)
|
||||
}
|
||||
|
||||
val androidHostTest by getting { dependencies { implementation(libs.androidx.test.runner) } }
|
||||
jvmTest.dependencies { implementation(compose.desktop.currentOs) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,28 +16,46 @@
|
|||
*/
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
@OptIn(ExperimentalTestApi::class, ExperimentalCoroutinesApi::class)
|
||||
class AlertHostTest {
|
||||
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alertHost_showsDialog_whenAlertIsTriggered() {
|
||||
fun alertHost_showsDialog_whenAlertIsTriggered() = runComposeUiTest {
|
||||
val alertManager = AlertManager()
|
||||
val title = "Alert Title"
|
||||
val message = "Alert Message"
|
||||
|
||||
composeTestRule.setContent { AlertHost(alertManager = alertManager) }
|
||||
setContent { AlertHost(alertManager = alertManager) }
|
||||
|
||||
alertManager.showAlert(title = title, message = message)
|
||||
|
||||
composeTestRule.onNodeWithText(title).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText(message).assertIsDisplayed()
|
||||
onNodeWithText(title).assertIsDisplayed()
|
||||
onNodeWithText(message).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
|
@ -18,27 +18,25 @@ package org.meshtastic.core.ui.component
|
|||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.test.assertDoesNotExist
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import org.meshtastic.core.ui.util.LocalBarcodeScannerSupported
|
||||
import org.meshtastic.core.ui.util.LocalNfcScannerSupported
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.Test
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class ImportFabUiTest {
|
||||
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun importFab_expands_onButtonClick_whenSupported() {
|
||||
fun importFab_expands_onButtonClick_whenSupported() = runComposeUiTest {
|
||||
val testTag = "import_fab"
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
CompositionLocalProvider(
|
||||
LocalBarcodeScannerSupported provides true,
|
||||
LocalNfcScannerSupported provides true,
|
||||
|
|
@ -48,18 +46,18 @@ class ImportFabUiTest {
|
|||
}
|
||||
|
||||
// Expand the FAB
|
||||
composeTestRule.onNodeWithTag(testTag).performClick()
|
||||
onNodeWithTag(testTag).performClick()
|
||||
|
||||
// Verify menu items are visible using their tags
|
||||
composeTestRule.onNodeWithTag("nfc_import").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("qr_import").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTag("url_import").assertIsDisplayed()
|
||||
onNodeWithTag("nfc_import").assertIsDisplayed()
|
||||
onNodeWithTag("qr_import").assertIsDisplayed()
|
||||
onNodeWithTag("url_import").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun importFab_hidesNfcAndQr_whenNotSupported() {
|
||||
fun importFab_hidesNfcAndQr_whenNotSupported() = runComposeUiTest {
|
||||
val testTag = "import_fab"
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
CompositionLocalProvider(
|
||||
LocalBarcodeScannerSupported provides false,
|
||||
LocalNfcScannerSupported provides false,
|
||||
|
|
@ -69,41 +67,41 @@ class ImportFabUiTest {
|
|||
}
|
||||
|
||||
// Expand the FAB
|
||||
composeTestRule.onNodeWithTag(testTag).performClick()
|
||||
onNodeWithTag(testTag).performClick()
|
||||
|
||||
// Verify menu items are visible using their tags
|
||||
composeTestRule.onNodeWithTag("nfc_import").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("qr_import").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithTag("url_import").assertIsDisplayed()
|
||||
onNodeWithTag("nfc_import").assertDoesNotExist()
|
||||
onNodeWithTag("qr_import").assertDoesNotExist()
|
||||
onNodeWithTag("url_import").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun importFab_showsUrlDialog_whenUrlItemClicked() {
|
||||
fun importFab_showsUrlDialog_whenUrlItemClicked() = runComposeUiTest {
|
||||
val testTag = "import_fab"
|
||||
composeTestRule.setContent { MeshtasticImportFAB(onImport = {}, isContactContext = true, testTag = testTag) }
|
||||
setContent { MeshtasticImportFAB(onImport = {}, isContactContext = true, testTag = testTag) }
|
||||
|
||||
composeTestRule.onNodeWithTag(testTag).performClick()
|
||||
composeTestRule.onNodeWithTag("url_import").performClick()
|
||||
onNodeWithTag(testTag).performClick()
|
||||
onNodeWithTag("url_import").performClick()
|
||||
|
||||
// The URL dialog should be shown.
|
||||
// We'll search for its title indirectly or check if an AlertDialog appeared.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun importFab_showsShareChannels_whenCallbackProvided() {
|
||||
fun importFab_showsShareChannels_whenCallbackProvided() = runComposeUiTest {
|
||||
val testTag = "import_fab"
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
MeshtasticImportFAB(onImport = {}, onShareChannels = {}, isContactContext = false, testTag = testTag)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(testTag).performClick()
|
||||
composeTestRule.onNodeWithTag("share_channels").assertIsDisplayed()
|
||||
onNodeWithTag(testTag).performClick()
|
||||
onNodeWithTag("share_channels").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun importFab_showsSharedContactDialog_whenProvided() {
|
||||
fun importFab_showsSharedContactDialog_whenProvided() = runComposeUiTest {
|
||||
val contact = SharedContact(user = User(long_name = "Suzume Goddess"), node_num = 1)
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
MeshtasticImportFAB(
|
||||
onImport = {},
|
||||
sharedContact = contact,
|
||||
|
|
@ -113,6 +111,6 @@ class ImportFabUiTest {
|
|||
}
|
||||
|
||||
// Check if goddess is here
|
||||
composeTestRule.onNodeWithText("Importing Suzume Goddess").assertIsDisplayed()
|
||||
onNodeWithText("Importing Suzume Goddess").assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
|
@ -18,22 +18,21 @@ package org.meshtastic.core.ui.util
|
|||
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class AlertManagerUiTest {
|
||||
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
private val alertManager = AlertManager()
|
||||
|
||||
@Test
|
||||
fun alertManager_showsAlert_whenRequested() {
|
||||
composeTestRule.setContent {
|
||||
fun alertManager_showsAlert_whenRequested() = runComposeUiTest {
|
||||
val alertManager = AlertManager()
|
||||
setContent {
|
||||
val alertData by alertManager.currentAlert.collectAsState()
|
||||
alertData?.let { data -> AlertPreviewRenderer(data) }
|
||||
}
|
||||
|
|
@ -43,29 +42,24 @@ class AlertManagerUiTest {
|
|||
|
||||
alertManager.showAlert(title = title, message = message)
|
||||
|
||||
composeTestRule.onNodeWithText(title).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText(message).assertIsDisplayed()
|
||||
onNodeWithText(title).assertIsDisplayed()
|
||||
onNodeWithText(message).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alertManager_confirmButton_triggersCallbackAndDismisses() {
|
||||
fun alertManager_confirmButton_triggersCallbackAndDismisses() = runComposeUiTest {
|
||||
val alertManager = AlertManager()
|
||||
var confirmClicked = false
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
val alertData by alertManager.currentAlert.collectAsState()
|
||||
alertData?.let { data -> AlertPreviewRenderer(data) }
|
||||
}
|
||||
|
||||
alertManager.showAlert(title = "Confirm Title", onConfirm = { confirmClicked = true })
|
||||
|
||||
// Default confirm text is "Okay" from resources, but AlertPreviewRenderer uses it
|
||||
// We'll search for the text "Okay" (assuming it matches the resource value)
|
||||
// Since we are in a test, we might need to use a hardcoded string or a resource
|
||||
// But for this test, let's just use the confirmText parameter to be sure
|
||||
alertManager.showAlert(title = "Confirm Title", confirmText = "Yes", onConfirm = { confirmClicked = true })
|
||||
|
||||
composeTestRule.onNodeWithText("Yes").performClick()
|
||||
onNodeWithText("Yes").performClick()
|
||||
|
||||
assert(confirmClicked)
|
||||
composeTestRule.onNodeWithText("Confirm Title").assertDoesNotExist()
|
||||
assertTrue(confirmClicked)
|
||||
onNodeWithText("Confirm Title").assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
|
@ -161,16 +161,16 @@ Android uses `@Module`-annotated classes (`CoreDataModule`, `CoreBleAndroidModul
|
|||
|
||||
### D1. Zero `commonTest` in feature modules *(resolved 2026-03-12)*
|
||||
|
||||
| Module | `commonTest` | `test`/`androidUnitTest` | `androidTest` |
|
||||
|---|---:|---:|---:|
|
||||
| `feature:settings` | 22 | 20 | 15 |
|
||||
| `feature:node` | 24 | 9 | 0 |
|
||||
| `feature:messaging` | 18 | 5 | 3 |
|
||||
| `feature:connections` | 27 | 0 | 0 |
|
||||
| `feature:firmware` | 15 | 25 | 0 |
|
||||
| `feature:wifi-provision` | 62 | 0 | 0 |
|
||||
| Module | `commonTest` | `test`/`androidUnitTest` |
|
||||
|---|---:|---:|
|
||||
| `feature:settings` | 33 | 20 |
|
||||
| `feature:node` | 24 | 9 |
|
||||
| `feature:messaging` | 21 | 5 |
|
||||
| `feature:connections` | 27 | 0 |
|
||||
| `feature:firmware` | 15 | 25 |
|
||||
| `feature:wifi-provision` | 62 | 0 |
|
||||
|
||||
**Outcome:** All 8 feature modules now have `commonTest` coverage (193 shared tests). Combined with 70 platform unit tests and 18 instrumented tests, feature modules have 281 tests total.
|
||||
**Outcome:** All 8 feature modules now have `commonTest` coverage (211 shared tests). Combined with 70 platform unit tests, feature modules have 281 tests total. All Compose UI tests have been migrated from `androidTest` to `commonTest` using CMP `runComposeUiTest`; instrumented test infrastructure has been removed from CI.
|
||||
|
||||
### D2. No shared test fixtures *(resolved 2026-03-12)*
|
||||
|
||||
|
|
|
|||
|
|
@ -59,13 +59,5 @@ kotlin {
|
|||
androidMain.dependencies { implementation(libs.markdown.renderer.android) }
|
||||
|
||||
commonTest.dependencies { implementation(projects.core.testing) }
|
||||
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,12 +38,5 @@ kotlin {
|
|||
|
||||
implementation(libs.jetbrains.navigation3.ui)
|
||||
}
|
||||
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,12 +43,5 @@ kotlin {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.di)
|
||||
}
|
||||
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ kotlin {
|
|||
android {
|
||||
namespace = "org.meshtastic.feature.messaging"
|
||||
androidResources.enable = false
|
||||
withHostTest { isIncludeAndroidResources = true }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
|
@ -56,6 +55,8 @@ kotlin {
|
|||
|
||||
androidMain.dependencies { implementation(libs.androidx.work.runtime.ktx) }
|
||||
|
||||
val androidHostTest by getting { dependencies { implementation(libs.androidx.work.testing) } }
|
||||
commonTest.dependencies { implementation(libs.compose.multiplatform.ui.test) }
|
||||
|
||||
jvmTest.dependencies { implementation(compose.desktop.currentOs) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,25 +16,21 @@
|
|||
*/
|
||||
package org.meshtastic.feature.messaging.component
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.tooling.preview.NodePreviewParameterProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.Message
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import kotlin.test.Test
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class MessageItemTest {
|
||||
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun mqttIconIsDisplayedWhenViaMqttIsTrue() {
|
||||
fun mqttIconIsDisplayedWhenViaMqttIsTrue() = runComposeUiTest {
|
||||
val testNode = NodePreviewParameterProvider().minnieMouse
|
||||
val messageWithMqtt =
|
||||
Message(
|
||||
|
|
@ -56,7 +52,7 @@ class MessageItemTest {
|
|||
viaMqtt = true,
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
MessageItem(
|
||||
message = messageWithMqtt,
|
||||
node = testNode,
|
||||
|
|
@ -69,11 +65,11 @@ class MessageItemTest {
|
|||
}
|
||||
|
||||
// Check that the MQTT icon is displayed
|
||||
composeTestRule.onNodeWithContentDescription("via MQTT").assertIsDisplayed()
|
||||
onNodeWithContentDescription("via MQTT").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mqttIconIsNotDisplayedWhenViaMqttIsFalse() {
|
||||
fun mqttIconIsNotDisplayedWhenViaMqttIsFalse() = runComposeUiTest {
|
||||
val testNode = NodePreviewParameterProvider().minnieMouse
|
||||
val messageWithoutMqtt =
|
||||
Message(
|
||||
|
|
@ -95,7 +91,7 @@ class MessageItemTest {
|
|||
viaMqtt = false,
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
MessageItem(
|
||||
message = messageWithoutMqtt,
|
||||
node = testNode,
|
||||
|
|
@ -108,11 +104,11 @@ class MessageItemTest {
|
|||
}
|
||||
|
||||
// Check that the MQTT icon is not displayed
|
||||
composeTestRule.onNodeWithContentDescription("via MQTT").assertDoesNotExist()
|
||||
onNodeWithContentDescription("via MQTT").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageItem_hasCorrectSemanticContentDescription() {
|
||||
fun messageItem_hasCorrectSemanticContentDescription() = runComposeUiTest {
|
||||
val testNode = NodePreviewParameterProvider().minnieMouse
|
||||
val message =
|
||||
Message(
|
||||
|
|
@ -134,7 +130,7 @@ class MessageItemTest {
|
|||
viaMqtt = false,
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
MessageItem(
|
||||
message = message,
|
||||
node = testNode,
|
||||
|
|
@ -147,8 +143,6 @@ class MessageItemTest {
|
|||
}
|
||||
|
||||
// Verify that the node containing the message text exists and matches the text
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Message from ${testNode.user.long_name}: Hello World")
|
||||
.assertIsDisplayed()
|
||||
onNodeWithContentDescription("Message from ${testNode.user.long_name}: Hello World").assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
|
@ -62,14 +62,5 @@ kotlin {
|
|||
}
|
||||
|
||||
androidMain.dependencies { implementation(libs.markdown.renderer.android) }
|
||||
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.junit)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.compose.ui.test.junit4)
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ kotlin {
|
|||
android {
|
||||
namespace = "org.meshtastic.feature.settings"
|
||||
androidResources.enable = false
|
||||
withHostTest { isIncludeAndroidResources = true }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
|
@ -57,17 +56,11 @@ kotlin {
|
|||
implementation(libs.androidx.appcompat)
|
||||
}
|
||||
|
||||
commonTest.dependencies { implementation(project(":core:datastore")) }
|
||||
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(project(":core:datastore"))
|
||||
implementation(libs.junit)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.compose.ui.test.junit4)
|
||||
implementation(libs.androidx.compose.ui.test.manifest)
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
}
|
||||
commonTest.dependencies {
|
||||
implementation(project(":core:datastore"))
|
||||
implementation(libs.compose.multiplatform.ui.test)
|
||||
}
|
||||
|
||||
jvmTest.dependencies { implementation(compose.desktop.currentOs) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.i_agree
|
||||
import org.meshtastic.core.resources.map_reporting
|
||||
import org.meshtastic.core.resources.map_reporting_summary
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MapReportingPreferenceTest {
|
||||
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
private fun getString(id: Int): String = InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
|
||||
|
||||
var mapReportingEnabled = false
|
||||
var shouldReportLocation = false
|
||||
var positionPrecision = 5
|
||||
var positionReportingInterval = 60
|
||||
|
||||
var mapReportingEnabledChanged = { enabled: Boolean -> mapReportingEnabled = enabled }
|
||||
var shouldReportLocationChanged = { enabled: Boolean -> shouldReportLocation = enabled }
|
||||
var positionPrecisionChanged = { precision: Int -> positionPrecision = precision }
|
||||
var positionReportingIntervalChanged = { interval: Int -> positionReportingInterval = interval }
|
||||
|
||||
private fun testMapReportingPreference() = composeTestRule.setContent {
|
||||
Column {
|
||||
MapReportingPreference(
|
||||
mapReportingEnabled = mapReportingEnabled,
|
||||
shouldReportLocation = shouldReportLocation,
|
||||
positionPrecision = positionPrecision,
|
||||
onMapReportingEnabledChanged = mapReportingEnabledChanged,
|
||||
onShouldReportLocationChanged = shouldReportLocationChanged,
|
||||
onPositionPrecisionChanged = positionPrecisionChanged,
|
||||
publishIntervalSecs = positionReportingInterval,
|
||||
onPublishIntervalSecsChanged = positionReportingIntervalChanged,
|
||||
enabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMapReportingPreference_showsText() {
|
||||
composeTestRule.apply {
|
||||
testMapReportingPreference()
|
||||
// Verify that the dialog title is displayed
|
||||
onNodeWithText(getString(Res.string.map_reporting)).assertIsDisplayed()
|
||||
onNodeWithText(getString(Res.string.map_reporting_summary)).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMapReportingPreference_toggleMapReporting() {
|
||||
composeTestRule.apply {
|
||||
testMapReportingPreference()
|
||||
onNodeWithText(getString(Res.string.i_agree)).assertIsNotDisplayed()
|
||||
onNodeWithText(getString(Res.string.map_reporting)).performClick()
|
||||
Assert.assertFalse(mapReportingEnabled)
|
||||
Assert.assertFalse(shouldReportLocation)
|
||||
onNodeWithText(getString(Res.string.i_agree)).assertIsDisplayed()
|
||||
onNodeWithText(getString(Res.string.i_agree)).performClick()
|
||||
Assert.assertTrue(shouldReportLocation)
|
||||
Assert.assertTrue(mapReportingEnabled)
|
||||
onNodeWithText(getString(Res.string.map_reporting)).performClick()
|
||||
onNodeWithText(getString(Res.string.i_agree)).assertIsNotDisplayed()
|
||||
Assert.assertTrue(shouldReportLocation)
|
||||
Assert.assertFalse(mapReportingEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,17 +23,14 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.debug_active_filters
|
||||
import org.meshtastic.core.resources.debug_default_search
|
||||
|
|
@ -42,18 +39,15 @@ import org.meshtastic.core.resources.getString
|
|||
import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog
|
||||
import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchMatch
|
||||
import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchState
|
||||
import org.robolectric.annotation.Config
|
||||
import kotlin.test.Test
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(sdk = [34])
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class DebugSearchTest {
|
||||
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun debugSearchBar_showsPlaceholder() {
|
||||
fun debugSearchBar_showsPlaceholder() = runComposeUiTest {
|
||||
val placeholder = getString(Res.string.debug_default_search)
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
DebugSearchBar(
|
||||
searchState = SearchState(),
|
||||
onSearchTextChange = {},
|
||||
|
|
@ -62,13 +56,13 @@ class DebugSearchTest {
|
|||
onClearSearch = {},
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText(placeholder).assertIsDisplayed()
|
||||
onNodeWithText(placeholder).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun debugSearchBar_showsClearButtonWhenTextEntered() {
|
||||
fun debugSearchBar_showsClearButtonWhenTextEntered() = runComposeUiTest {
|
||||
val placeholder = getString(Res.string.debug_default_search)
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
var searchText by remember { mutableStateOf("test") }
|
||||
DebugSearchBar(
|
||||
searchState = SearchState(searchText = searchText),
|
||||
|
|
@ -78,17 +72,17 @@ class DebugSearchTest {
|
|||
onClearSearch = { searchText = "" },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithContentDescription("Clear search").assertIsDisplayed().performClick()
|
||||
composeTestRule.onNodeWithText(placeholder).assertIsDisplayed()
|
||||
onNodeWithContentDescription("Clear search").assertIsDisplayed().performClick()
|
||||
onNodeWithText(placeholder).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun debugSearchBar_searchFor_showsArrowsClearAndValues() {
|
||||
fun debugSearchBar_searchFor_showsArrowsClearAndValues() = runComposeUiTest {
|
||||
val searchText = "test"
|
||||
val matchCount = 3
|
||||
val currentMatchIndex = 1
|
||||
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
DebugSearchBar(
|
||||
searchState =
|
||||
SearchState(
|
||||
|
|
@ -104,18 +98,18 @@ class DebugSearchTest {
|
|||
)
|
||||
}
|
||||
// Check the match count display (e.g., '2/3')
|
||||
composeTestRule.onNodeWithText("${currentMatchIndex + 1}/$matchCount").assertIsDisplayed()
|
||||
onNodeWithText("${currentMatchIndex + 1}/$matchCount").assertIsDisplayed()
|
||||
// Check the navigation arrows
|
||||
composeTestRule.onNodeWithContentDescription("Previous match").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithContentDescription("Next match").assertIsDisplayed()
|
||||
onNodeWithContentDescription("Previous match").assertIsDisplayed()
|
||||
onNodeWithContentDescription("Next match").assertIsDisplayed()
|
||||
// Check the clear button
|
||||
composeTestRule.onNodeWithContentDescription("Clear search").assertIsDisplayed()
|
||||
onNodeWithContentDescription("Clear search").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun debugFilterBar_showsFilterButtonAndMenu() {
|
||||
fun debugFilterBar_showsFilterButtonAndMenu() = runComposeUiTest {
|
||||
val filterLabel = getString(Res.string.debug_filters)
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
var filterTexts by remember { mutableStateOf(listOf<String>()) }
|
||||
var customFilterText by remember { mutableStateOf("") }
|
||||
val presetFilters = listOf("Error", "Warning", "Info")
|
||||
|
|
@ -138,13 +132,13 @@ class DebugSearchTest {
|
|||
)
|
||||
}
|
||||
// The filter button should be visible
|
||||
composeTestRule.onNodeWithText(filterLabel).assertIsDisplayed()
|
||||
onNodeWithText(filterLabel).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun debugFilterBar_addCustomFilter_displaysActiveFilter() {
|
||||
fun debugFilterBar_addCustomFilter_displaysActiveFilter() = runComposeUiTest {
|
||||
val activeFiltersLabel = getString(Res.string.debug_active_filters)
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
var filterTexts by remember { mutableStateOf(listOf<String>()) }
|
||||
var customFilterText by remember { mutableStateOf("") }
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
|
|
@ -162,18 +156,16 @@ class DebugSearchTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
with(composeTestRule) {
|
||||
onNodeWithText("Add custom filter").performTextInput("MyFilter")
|
||||
onNodeWithContentDescription("Add filter").performClick()
|
||||
onNodeWithText(activeFiltersLabel).assertIsDisplayed()
|
||||
onNodeWithText("MyFilter").assertIsDisplayed()
|
||||
}
|
||||
onNodeWithText("Add custom filter").performTextInput("MyFilter")
|
||||
onNodeWithContentDescription("Add filter").performClick()
|
||||
onNodeWithText(activeFiltersLabel).assertIsDisplayed()
|
||||
onNodeWithText("MyFilter").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun debugActiveFilters_clearAllFilters_removesFilters() {
|
||||
fun debugActiveFilters_clearAllFilters_removesFilters() = runComposeUiTest {
|
||||
val activeFiltersLabel = getString(Res.string.debug_active_filters)
|
||||
composeTestRule.setContent {
|
||||
setContent {
|
||||
var filterTexts by remember { mutableStateOf(listOf("A", "B")) }
|
||||
DebugActiveFilters(
|
||||
filterTexts = filterTexts,
|
||||
|
|
@ -183,13 +175,13 @@ class DebugSearchTest {
|
|||
)
|
||||
}
|
||||
// The active filters label and chips should be visible
|
||||
composeTestRule.onNodeWithText(activeFiltersLabel).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("A").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("B").assertIsDisplayed()
|
||||
onNodeWithText(activeFiltersLabel).assertIsDisplayed()
|
||||
onNodeWithText("A").assertIsDisplayed()
|
||||
onNodeWithText("B").assertIsDisplayed()
|
||||
// Click the clear all filters button
|
||||
composeTestRule.onNodeWithContentDescription("Clear all filters").performClick()
|
||||
onNodeWithContentDescription("Clear all filters").performClick()
|
||||
// The filter chips should no longer be visible
|
||||
composeTestRule.onNodeWithText("A").assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText("B").assertDoesNotExist()
|
||||
onNodeWithText("A").assertDoesNotExist()
|
||||
onNodeWithText("B").assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
|
@ -16,27 +16,24 @@
|
|||
*/
|
||||
package org.meshtastic.feature.settings.radio.component
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.save
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.Position
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class EditDeviceProfileDialogTest {
|
||||
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
private val title = "Export configuration"
|
||||
private val deviceProfile =
|
||||
DeviceProfile(
|
||||
|
|
@ -46,61 +43,61 @@ class EditDeviceProfileDialogTest {
|
|||
fixed_position = Position(latitude_i = 327766650, longitude_i = -967969890, altitude = 138),
|
||||
)
|
||||
|
||||
private fun testEditDeviceProfileDialog(onDismiss: () -> Unit = {}, onConfirm: (DeviceProfile) -> Unit = {}) =
|
||||
composeTestRule.setContent {
|
||||
@Test
|
||||
fun testEditDeviceProfileDialog_showsDialogTitle() = runComposeUiTest {
|
||||
setContent {
|
||||
EditDeviceProfileDialog(title = title, deviceProfile = deviceProfile, onConfirm = {}, onDismiss = {})
|
||||
}
|
||||
|
||||
// Verify that the dialog title is displayed
|
||||
onNodeWithText(title).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEditDeviceProfileDialog_showsCancelAndSaveButtons() = runComposeUiTest {
|
||||
setContent {
|
||||
EditDeviceProfileDialog(title = title, deviceProfile = deviceProfile, onConfirm = {}, onDismiss = {})
|
||||
}
|
||||
|
||||
// Verify the "Cancel" and "Save" buttons are displayed
|
||||
onNodeWithText(getString(Res.string.cancel)).assertIsDisplayed()
|
||||
onNodeWithText(getString(Res.string.save)).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEditDeviceProfileDialog_clickCancelButton() = runComposeUiTest {
|
||||
var onDismissClicked = false
|
||||
setContent {
|
||||
EditDeviceProfileDialog(
|
||||
title = title,
|
||||
deviceProfile = deviceProfile,
|
||||
onConfirm = onConfirm,
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = {},
|
||||
onDismiss = { onDismissClicked = true },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEditDeviceProfileDialog_showsDialogTitle() {
|
||||
composeTestRule.apply {
|
||||
testEditDeviceProfileDialog()
|
||||
|
||||
// Verify that the dialog title is displayed
|
||||
onNodeWithText(title).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEditDeviceProfileDialog_showsCancelAndSaveButtons() {
|
||||
composeTestRule.apply {
|
||||
testEditDeviceProfileDialog()
|
||||
|
||||
// Verify the "Cancel" and "Save" buttons are displayed
|
||||
onNodeWithText(getString(Res.string.cancel)).assertIsDisplayed()
|
||||
onNodeWithText(getString(Res.string.save)).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEditDeviceProfileDialog_clickCancelButton() {
|
||||
var onDismissClicked = false
|
||||
composeTestRule.apply {
|
||||
testEditDeviceProfileDialog(onDismiss = { onDismissClicked = true })
|
||||
|
||||
// Click the "Cancel" button
|
||||
onNodeWithText(getString(Res.string.cancel)).performClick()
|
||||
}
|
||||
// Click the "Cancel" button
|
||||
onNodeWithText(getString(Res.string.cancel)).performClick()
|
||||
|
||||
// Verify onDismiss is called
|
||||
Assert.assertTrue(onDismissClicked)
|
||||
assertTrue(onDismissClicked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEditDeviceProfileDialog_addChannels() {
|
||||
fun testEditDeviceProfileDialog_addChannels() = runComposeUiTest {
|
||||
var actualDeviceProfile: DeviceProfile? = null
|
||||
composeTestRule.apply {
|
||||
testEditDeviceProfileDialog(onConfirm = { actualDeviceProfile = it })
|
||||
|
||||
onNodeWithText(getString(Res.string.save)).performClick()
|
||||
setContent {
|
||||
EditDeviceProfileDialog(
|
||||
title = title,
|
||||
deviceProfile = deviceProfile,
|
||||
onConfirm = { actualDeviceProfile = it },
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
|
||||
onNodeWithText(getString(Res.string.save)).performClick()
|
||||
|
||||
// Verify onConfirm is called with the correct DeviceProfile
|
||||
Assert.assertEquals(deviceProfile, actualDeviceProfile)
|
||||
assertEquals(deviceProfile, actualDeviceProfile)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.i_agree
|
||||
import org.meshtastic.core.resources.map_reporting
|
||||
import org.meshtastic.core.resources.map_reporting_summary
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class MapReportingPreferenceTest {
|
||||
|
||||
var mapReportingEnabled = false
|
||||
var shouldReportLocation = false
|
||||
var positionPrecision = 5
|
||||
var positionReportingInterval = 60
|
||||
|
||||
var mapReportingEnabledChanged = { enabled: Boolean -> mapReportingEnabled = enabled }
|
||||
var shouldReportLocationChanged = { enabled: Boolean -> shouldReportLocation = enabled }
|
||||
var positionPrecisionChanged = { precision: Int -> positionPrecision = precision }
|
||||
var positionReportingIntervalChanged = { interval: Int -> positionReportingInterval = interval }
|
||||
|
||||
@Test
|
||||
fun testMapReportingPreference_showsText() = runComposeUiTest {
|
||||
setContent {
|
||||
Column {
|
||||
MapReportingPreference(
|
||||
mapReportingEnabled = mapReportingEnabled,
|
||||
shouldReportLocation = shouldReportLocation,
|
||||
positionPrecision = positionPrecision,
|
||||
onMapReportingEnabledChanged = mapReportingEnabledChanged,
|
||||
onShouldReportLocationChanged = shouldReportLocationChanged,
|
||||
onPositionPrecisionChanged = positionPrecisionChanged,
|
||||
publishIntervalSecs = positionReportingInterval,
|
||||
onPublishIntervalSecsChanged = positionReportingIntervalChanged,
|
||||
enabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
// Verify that the dialog title is displayed
|
||||
onNodeWithText(getString(Res.string.map_reporting)).assertIsDisplayed()
|
||||
onNodeWithText(getString(Res.string.map_reporting_summary)).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMapReportingPreference_toggleMapReporting() = runComposeUiTest {
|
||||
setContent {
|
||||
Column {
|
||||
MapReportingPreference(
|
||||
mapReportingEnabled = mapReportingEnabled,
|
||||
shouldReportLocation = shouldReportLocation,
|
||||
positionPrecision = positionPrecision,
|
||||
onMapReportingEnabledChanged = mapReportingEnabledChanged,
|
||||
onShouldReportLocationChanged = shouldReportLocationChanged,
|
||||
onPositionPrecisionChanged = positionPrecisionChanged,
|
||||
publishIntervalSecs = positionReportingInterval,
|
||||
onPublishIntervalSecsChanged = positionReportingIntervalChanged,
|
||||
enabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
onNodeWithText(getString(Res.string.i_agree)).assertDoesNotExist()
|
||||
onNodeWithText(getString(Res.string.map_reporting)).performClick()
|
||||
assertFalse(mapReportingEnabled)
|
||||
assertFalse(shouldReportLocation)
|
||||
onNodeWithText(getString(Res.string.i_agree)).assertIsDisplayed()
|
||||
onNodeWithText(getString(Res.string.i_agree)).performClick()
|
||||
assertTrue(shouldReportLocation)
|
||||
assertTrue(mapReportingEnabled)
|
||||
onNodeWithText(getString(Res.string.map_reporting)).performClick()
|
||||
onNodeWithText(getString(Res.string.i_agree)).assertDoesNotExist()
|
||||
assertTrue(shouldReportLocation)
|
||||
assertFalse(mapReportingEnabled)
|
||||
}
|
||||
}
|
||||
|
|
@ -129,6 +129,7 @@ compose-multiplatform-runtime = { module = "org.jetbrains.compose.runtime:runtim
|
|||
compose-multiplatform-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-ui-test = { module = "org.jetbrains.compose.ui:ui-test", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-multiplatform" }
|
||||
compose-multiplatform-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "compose-multiplatform-material3" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue