Skip to content

Commit 8827139

Browse files
committed
Android: introduce unit tests and Kotlin
1 parent ad5bb01 commit 8827139

8 files changed

Lines changed: 142 additions & 12 deletions

File tree

.github/workflows/ci-test.yml

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,34 @@ jobs:
2121
- name: Check public API compatibility
2222
run: npm run check:api-compat
2323

24-
android-test:
24+
android-unit-test:
2525
needs: lint
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v7
29+
30+
- uses: jdx/mise-action@v4
31+
32+
- name: Cache npm packages
33+
uses: actions/cache@v6
34+
with:
35+
path: ~/.npm
36+
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json', 'test/test.ts') }}
37+
restore-keys: |
38+
${{ runner.os }}-npm-
39+
40+
- name: Install dependencies
41+
run: npm install
42+
43+
- name: Setup Gradle
44+
uses: gradle/actions/setup-gradle@v6
45+
46+
- name: Run Android unit tests
47+
working-directory: android
48+
run: ./gradlew :app:test
49+
50+
android-test:
51+
needs: [lint, android-unit-test]
2652
runs-on: bitrise-react-native-code-push-linux-runner
2753
strategy:
2854
matrix:
@@ -49,6 +75,16 @@ jobs:
4975
- name: Install dependencies
5076
run: npm install
5177

78+
- name: Setup Gradle
79+
uses: gradle/actions/setup-gradle@v6
80+
with:
81+
# This job's Gradle build lives in a test app generated at runtime by the test harness,
82+
# not a project checked into this repo, so there's nothing meaningful for it to
83+
# contribute back to the cache. Read-only avoids each matrix variant (bare/expo) writing
84+
# its own redundant entry; it still reuses whatever android-unit-test wrote, since
85+
# setup-gradle shares its cache across jobs in the same workflow run.
86+
cache-read-only: true
87+
5288
- name: Enable KVM
5389
run: |
5490
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules

CLAUDE.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ React Native CodePush is a native module that enables over-the-air updates for R
77
## Development Commands
88

99
### Testing
10-
- `npm test` - Run all tests with TypeScript compilation
10+
11+
#### Unit tests
12+
13+
- `cd android && ./gradlew :app:test`
14+
- iOS: no unit tests yet.
15+
16+
#### E2E Tests
1117
- `npm run test:android` - Run Android-specific tests
1218
- `npm run test:ios` - Run iOS-specific tests
1319
- `npm run test:setup-android` - Set up Android emulator for testing
@@ -34,7 +40,7 @@ React Native CodePush is a native module that enables over-the-air updates for R
3440

3541
### Platform Structure
3642
- **iOS**: `ios/` - Objective-C implementation with CocoaPods integration
37-
- **Android**: `android/` - Java implementation with Gradle plugin
43+
- **Android**: `android/` - Java/Kotlin implementation with Gradle plugin
3844
- **Windows**: `windows/` - C++ implementation for Windows React Native
3945
- **JavaScript**: Root level - TypeScript definitions and bridge code
4046

@@ -48,7 +54,7 @@ React Native CodePush is a native module that enables over-the-air updates for R
4854
- **Custom Test Runner**: TypeScript-based test framework in `test/`
4955
- **Real App Testing**: Creates actual React Native apps for integration testing
5056
- **Scenario Testing**: Update, rollback, and error scenarios
51-
- **No unit test infra yet**: this repo only has the mocha-based integration suite above. `src/acquisition-sdk/__tests__/` contains tests ported from upstream `microsoft/code-push`, kept for future reference - they are deliberately not wired into `npm test` or any runner. Don't assume they're dead/forgotten code, and don't wire them in without setting up real unit test infra first.
57+
- **No unit test infra for JS/iOS yet**: JS/iOS only have the mocha-based integration suite above. `src/acquisition-sdk/__tests__/` contains tests ported from upstream `microsoft/code-push`, kept for future reference - they are deliberately not wired into `npm test` or any runner. Don't assume they're dead/forgotten code, and don't wire them in without setting up real unit test infra first.
5258
- **Templates**: `test/template/` holds native files (Podfile, AppDelegate, Android app files) and JS scenarios copied over top of a freshly generated RN/Expo app during test setup, overwriting its defaults — edit files here, not the generated project, for changes to persist
5359
- **`test:ios` vs `test:setup:ios` vs `test:fast:ios`**: `test:ios` is just `test:setup:ios` followed by `test:fast:ios` — the two are meant to be split apart for local iteration.
5460
- `test:setup:ios` (mocha `--ios --setup`) boots the simulator and provisions the test app once: copies templates, runs `pod install`, patches Info.plist/AppDelegate. It never builds or runs any test scenario.

Examples/CodePushDemo/android/gradle.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,9 @@ hermesEnabled=true
4242
# This allows your app to draw behind system bars for an immersive UI.
4343
# Note: Only works with ReactActivity and should not be used with custom Activity.
4444
edgeToEdgeEnabled=false
45+
46+
# Opt out of AGP 9's built-in Kotlin support and new DSL, matching what RN's own 0.87 app template
47+
# does. See the AGP v9 adoption RFC:
48+
# https://github.com/react-native-community/discussions-and-proposals/pull/1006).
49+
android.builtInKotlin=false
50+
android.newDsl=false

android/app/build.gradle

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
1-
apply plugin: "com.android.library"
1+
// No versions specified: both plugins are expected to already be resolved on the root project's
2+
// buildscript classpath, which every RN app template declares (AGP for the app itself, Kotlin
3+
// because RN ships Kotlin internally).
4+
// Matches how other RN libraries (e.g. reanimated) apply these plugins.
5+
plugins {
6+
id "com.android.library"
7+
id "org.jetbrains.kotlin.android" apply false
8+
}
9+
10+
// AGP 9 provides Kotlin support built in; applying the classic kotlin-android plugin on top of it
11+
// leads to a configuration-time failure. Below AGP 9, and on AGP 9+ when `android.builtInKotlin=false`
12+
// opts back out of built-in Kotlin, the classic plugin is still required.
13+
// See React Native RFC about ecosystem migration details: https://github.com/react-native-community/discussions-and-proposals/pull/1006
14+
def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
15+
def builtInKotlinEnabled = agpMajor >= 9 && (!project.hasProperty("android.builtInKotlin") || Boolean.parseBoolean(project.property("android.builtInKotlin").toString()))
16+
if (!builtInKotlinEnabled) {
17+
apply plugin: "org.jetbrains.kotlin.android"
18+
}
219

320
def isNewArchitectureEnabled() {
421
// To opt-in for the New Architecture, you can either:
@@ -10,15 +27,18 @@ def isNewArchitectureEnabled() {
1027

1128
def IS_NEW_ARCHITECTURE_ENABLED = isNewArchitectureEnabled()
1229

13-
def DEFAULT_COMPILE_SDK_VERSION = 26
14-
def DEFAULT_BUILD_TOOLS_VERSION = "26.0.3"
15-
def DEFAULT_TARGET_SDK_VERSION = 26
16-
def DEFAULT_MIN_SDK_VERSION = 16
30+
// These are fallbacks only. Keep them aligned with RN's current app template
31+
// so a consumer relying on the fallback still gets a build that actually works
32+
// with a current RN version.
33+
def DEFAULT_COMPILE_SDK_VERSION = 35
34+
def DEFAULT_BUILD_TOOLS_VERSION = "36.0.0"
35+
def DEFAULT_TARGET_SDK_VERSION = 35
36+
def DEFAULT_MIN_SDK_VERSION = 24
1737

1838
android {
1939
namespace "com.microsoft.codepush.react"
2040

21-
compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION
41+
compileSdk rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION
2242
buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION
2343

2444
defaultConfig {
@@ -40,9 +60,25 @@ android {
4060
buildFeatures {
4161
buildConfig true
4262
}
63+
64+
compileOptions {
65+
sourceCompatibility JavaVersion.VERSION_17
66+
targetCompatibility JavaVersion.VERSION_17
67+
}
68+
}
69+
70+
// Only set when we applied the classic kotlin-android plugin ourselves above: AGP 9's built-in
71+
// Kotlin integration doesn't expose this extension at all, per Android's own built-in
72+
// Kotlin migration guide: https://developer.android.com/build/migrate-to-built-in-kotlin
73+
if (!builtInKotlinEnabled) {
74+
android.kotlinOptions {
75+
jvmTarget = "17"
76+
}
4377
}
4478

4579
dependencies {
4680
implementation 'com.facebook.react:react-android:0.82.1'
4781
implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3'
82+
83+
testImplementation 'junit:junit:4.13.2'
4884
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.microsoft.codepush.react
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Assert.assertTrue
5+
import org.junit.Rule
6+
import org.junit.Test
7+
import org.junit.rules.TemporaryFolder
8+
import java.io.File
9+
10+
class FileUtilsTest {
11+
12+
@get:Rule
13+
val tempFolder = TemporaryFolder()
14+
15+
@Test
16+
fun copyDirectoryContents_copiesNestedFilesAndSubdirectories() {
17+
// Given
18+
val sourceDir = tempFolder.newFolder("source")
19+
File(sourceDir, "root.txt").writeText("root contents")
20+
val nestedDir = File(sourceDir, "nested").apply { mkdir() }
21+
File(nestedDir, "child.txt").writeText("child contents")
22+
23+
val destinationDir = File(tempFolder.root, "destination")
24+
25+
// When
26+
FileUtils.copyDirectoryContents(sourceDir.absolutePath, destinationDir.absolutePath)
27+
28+
// Then
29+
assertEquals("root contents", File(destinationDir, "root.txt").readText())
30+
val copiedNestedFile = File(destinationDir, "nested/child.txt")
31+
assertTrue(copiedNestedFile.exists())
32+
assertEquals("child contents", copiedNestedFile.readText())
33+
}
34+
}

android/build.gradle

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ buildscript {
66
mavenCentral()
77
}
88
dependencies {
9-
classpath("com.android.tools.build:gradle:8.12.0")
9+
// Must match (or exceed) the AGP version RN's own @react-native/gradle-plugin declares: that plugin is
10+
// included as a composite build below and its own AGP dependency wins Gradle's classpath
11+
// conflict resolution regardless of what's pinned here, so an out-of-date pin here is
12+
// silently overridden rather than actually enforced. Bump this in lockstep with RN's own
13+
// gradle-plugin version whenever bumping the react-native devDependency.
14+
classpath("com.android.tools.build:gradle:9.2.1")
1015
classpath("com.facebook.react:react-native-gradle-plugin")
1116

1217
// NOTE: Do not place your application dependencies here; they belong

android/gradle.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,10 @@
1616
# This option should only be used with decoupled projects. More details, visit
1717
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
1818
# org.gradle.parallel=true
19+
android.useAndroidX=true
20+
21+
# https://github.com/react-native-community/discussions-and-proposals/pull/1006
22+
# Applies to this repo's own standalone build/test harness only; see android/app/build.gradle's comments
23+
# for why downstream consumers may still land in either mode.
24+
android.builtInKotlin=false
25+
android.newDsl=false

android/gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
33
zipStoreBase=GRADLE_USER_HOME
44
zipStorePath=wrapper/dists
5-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
5+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-all.zip

0 commit comments

Comments
 (0)