Skip to content

Commit 84a44db

Browse files
committed
test(native): expand Robolectric and XCTest beyond harness smoke
1 parent 07c1313 commit 84a44db

11 files changed

Lines changed: 555 additions & 74 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Root `yarn` installs all workspaces (library under `packages/core/`, `RNGoogleMo
8181

8282
Appium 3 + WebdriverIO lives in `tooling/appium/` (`@invertase/rngma-appium`). Pin contract: `yarn.lock` plus `tooling/appium/drivers.manifest.json`. Device-free check: `yarn tests:appium:validate`. Drivers: `yarn tests:appium:drivers:install` then `yarn tests:appium:drivers:verify`. Device smoke: `yarn tests:appium:android` / `yarn tests:appium:ios` after build/install prerequisites. Details: [Appium](okf-bundle/testing/running-e2e.md#appium-scaffold).
8383

84-
Android **Java** format: `yarn lint:android`. Android **Kotlin** format: repo-root `./gradlew ktlintFormat` (check-only: `./gradlew ktlintCheck`). Optional: `./gradlew addKtlintFormatGitPreCommitHook` to install a local hook; Invertase global pre-commit also invokes root `./gradlew ktlintFormat` when present (this repo does not ship an installed hook). Example app builds remain under `RNGoogleMobileAdsExample/android/`. Native unit skeletons: `yarn tests:android:unit` (Robolectric) and `yarn tests:ios:unit` (XCTest) — see [agent command policy](okf-bundle/testing/agent-command-policy.md).
84+
Android **Java** format: `yarn lint:android`. Android **Kotlin** format: repo-root `./gradlew ktlintFormat` (check-only: `./gradlew ktlintCheck`). Optional: `./gradlew addKtlintFormatGitPreCommitHook` to install a local hook; Invertase global pre-commit also invokes root `./gradlew ktlintFormat` when present (this repo does not ship an installed hook). Example app builds remain under `RNGoogleMobileAdsExample/android/`. Native unit tests (owned mappers/helpers; no Google auction/fill asserts): `yarn tests:android:unit` (Robolectric) and `yarn tests:ios:unit` (XCTest) — see [agent command policy](okf-bundle/testing/agent-command-policy.md).
8585

8686
## Submitting code for review
8787

‎okf-bundle/testing/agent-command-policy.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ If a command is not listed (or linked) here, **do not run it**. E2e: [local e2e
2626
| `packages/core/src/` → `lib/` + plugin | `yarn prepare` (runs `yarn lerna:prepare`) | ad-hoc bob/babel/tsc; skip Lerna/Nx env |
2727
| TS check | `yarn tsc:compile` | invented `tsc` configs |
2828
| Jest | `yarn tests:jest`, `yarn tests:jest-coverage` | `npx jest` random cwd |
29-
| Android JVM unit (Robolectric) | `yarn tests:android:unit` → example `./gradlew :react-native-google-mobile-ads:testDebugUnitTest` (sources under `packages/core/android/src/test/`) | ad-hoc `./gradlew … testDebugUnitTest` outside this yarn script; treating library-folder standalone AGP as the gate |
30-
| iOS XCTest unit | `yarn tests:ios:unit` → `xcodebuild test` on `packages/core/ios/RNGoogleMobileAdsUnitTests.xcodeproj` (sources under `packages/core/ios/Tests/`; podspec `UnitTests` test_spec mirrors them) | ad-hoc `xcodebuild test` outside this yarn script |
29+
| Android JVM unit (Robolectric) | `yarn tests:android:unit` → example `./gradlew :react-native-google-mobile-ads:testDebugUnitTest` (sources under `packages/core/android/src/test/`; owned mapper/helper tests + harness smoke) | ad-hoc `./gradlew … testDebugUnitTest` outside this yarn script; treating library-folder standalone AGP as the gate |
30+
| iOS XCTest unit | `yarn tests:ios:unit` → `xcodebuild test` on `packages/core/ios/RNGoogleMobileAdsUnitTests.xcodeproj` (sources under `packages/core/ios/Tests/`; lightweight harness also compiles `RNGoogleMobileAdsOwnedMappers`; podspec `UnitTests` test_spec mirrors Tests) | ad-hoc `xcodebuild test` outside this yarn script |
3131
| Lint (local) | [lint-by-tree](validation-checklist.md#lint-and-formatting) (`yarn lint:js` / `yarn lint:android` / `yarn lint:ios:check`; check vs `:fix`/`--replace` by work type) | ad-hoc eslint; `npx google-java-format`; invented `clang-format` / prettier |
3232
| Kotlin check (Android `.kt`) | repo-root `./gradlew ktlintCheck` (frozen review / check-only) | ad-hoc example `gradlew`; inventing yarn wrappers for ktlint |
3333
| Kotlin format (Android `.kt`) | repo-root `./gradlew ktlintFormat` (optional `-PinternalKtlintGitFilter=…`; implementation / documentation) | ad-hoc example `gradlew`; inventing yarn wrappers for ktlint |

‎okf-bundle/testing/coverage-design.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ timestamp: 2026-09-04T00:00:00Z
1212
|-------|---------|
1313
| Jest (`packages/core/src/**`) | `yarn tests:jest-coverage` — only if `packages/core/src/` or `packages/core/plugin/` TS was touched. `packages/core/src/version.ts` is generated by `yarn prepare`; it is out of the [touched-line bar](#touched-line-bar), not a Jest config ignore. **TS ownership stays here** even after native coverage adopt. |
1414
| Plugin TS | root `yarn tests:jest-coverage packages/core/plugin/__tests__/` (or touched plugin paths). Not `packages/core/plugin/jest.config.js`. |
15-
| Android JVM unit | `yarn tests:android:unit` — Robolectric under `packages/core/android/src/test/` when that tree (or Robolectric wiring) changed. Harness only until product mapper tests land. |
16-
| iOS XCTest unit | `yarn tests:ios:unit` — XCTest under `packages/core/ios/Tests/` (+ `RNGoogleMobileAdsUnitTests.xcodeproj`) when that tree changed. Podspec `UnitTests` test_spec mirrors the same sources for CocoaPods consumers. |
15+
| Android JVM unit | `yarn tests:android:unit` — Robolectric under `packages/core/android/src/test/` when that tree (or Robolectric wiring) changed. Includes owned mapper/helper tests (`ReactNativeGoogleMobileAdsCommon`, `SharedUtils`); no Google auction/fill asserts. |
16+
| iOS XCTest unit | `yarn tests:ios:unit` — XCTest under `packages/core/ios/Tests/` (+ `RNGoogleMobileAdsUnitTests.xcodeproj`, which also compiles `RNGoogleMobileAdsOwnedMappers` for the lightweight harness) when that tree changed. Podspec `UnitTests` test_spec mirrors `ios/Tests/**`; owned mappers ship in the main pod `source_files`. A production extract into `RNGoogleMobileAds/*` (or `Common.mm` delegate) is **not** harness-only — [platform coverage](running-e2e.md#platform-coverage-gate-blocking) plugin/native still requires iOS Appium for that Diff. |
1717
| E2e | [local e2e](running-e2e.md#local-e2e-commands) when [platform coverage](running-e2e.md#platform-coverage-gate-blocking) requires it |
1818
| Native e2e (Jacoco / LLVM) | Published [`react-native-coverage@0.2.0`](https://www.npmjs.com/package/react-native-coverage) in the **example harness only** (Pattern C). See [§ native agent collection](#native-agent-collection). |
1919

‎okf-bundle/testing/running-e2e.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Private workspace `@invertase/rngma-appium` at `tooling/appium/` (Yarn workspace
6565
| Pure path relocate / workspace layout (no semantic change to native, specs, or plugin output) | Keep named scripts valid; **not** native e2e. **Supersedes** the `RNGoogleMobileAdsExample/**` row when example edits are only path/`workspace:`/import-path updates for that relocate (no runtime or native example behavior change). |
6666
| Appium config / drivers / testIDs-only (`tooling/appium/**` drivers/validate/helpers and/or example `testID` props only; no format gallery behavior change; no device specs change) | `yarn tests:appium:validate`. **Not** device Appium. **Supersedes** the example Appium device row for those edits. |
6767
| Kotlin style-only / ktlint format on `packages/core/android/**` (`.kt` whitespace/style only; no semantic native behavior change) | **Not** native e2e. Same spirit as pure path relocate. **Supersedes** the `packages/core/android/**` clause of the plugin/native row when android edits are ktlint format/style-only. |
68-
| Native unit-test harness only (`packages/core/android/src/test/**` and/or `packages/core/ios/Tests/**`, plus Robolectric/XCTest wiring in `packages/core/android/build.gradle`, podspec `test_spec` / `exclude_files`, or `packages/core/ios/RNGoogleMobileAdsUnitTests.xcodeproj`; no production native behavior change) | Touched platforms: `yarn tests:android:unit` and/or `yarn tests:ios:unit`. **Not** native e2e. **Supersedes** the plugin/native row for those harness-only edits. |
68+
| Native unit-test harness only (`packages/core/android/src/test/**` and/or `packages/core/ios/Tests/**`, plus Robolectric/XCTest wiring in `packages/core/android/build.gradle`, podspec `test_spec` / `exclude_files`, or `packages/core/ios/RNGoogleMobileAdsUnitTests.xcodeproj`; no production native behavior change) | Touched platforms: `yarn tests:android:unit` and/or `yarn tests:ios:unit`. **Not** native e2e. **Supersedes** the plugin/native row for those harness-only edits. Does **not** apply when the Diff also touches production `packages/core/ios/RNGoogleMobileAds/**` / `packages/core/android/src/main/**` (e.g. OwnedMappers extract + `Common` delegate) — that stays on the plugin/native Appium row for the affected platform. |
6969
| Touched `tooling/appium/test/specs/**` or Appium format smoke behavior | Android: `yarn tests:packager` (or `:reset-cache` when [pre-flight](#pre-flight) says free `:8081`) + `yarn tests:android:build` + `yarn tests:appium:android`. iOS: `yarn tests:packager` (same reset) + `yarn tests:ios:pod:install` + `yarn tests:ios:run` + `yarn tests:appium:ios`. Specs that changed, on the platform(s) those specs exercise. [Tee](#local-e2e-commands). |
7070
| `RNGoogleMobileAdsExample/**` (example app/config, not `node_modules`) | Android: `yarn tests:packager` (or `:reset-cache` when [pre-flight](#pre-flight) says free `:8081`) + `yarn tests:android:build` + `yarn tests:appium:android`. iOS: `yarn tests:packager` (same reset) + `yarn tests:ios:pod:install` + `yarn tests:ios:run` + `yarn tests:appium:ios`. Each platform the example change can affect. [Tee](#local-e2e-commands). Does **not** apply when the pure path relocate / workspace layout row **or** the Appium config / testIDs-only row already covers the example edits. |
7171
| Plugin output that changes native manifests/plists, or touched `packages/core/android/**`, `packages/core/ios/**`, podspec, or `packages/core/src/specs/**` | Android: `yarn tests:packager` (or `:reset-cache` when [pre-flight](#pre-flight) says free `:8081`) + `yarn tests:android:build` + `yarn tests:appium:android`. iOS: `yarn tests:packager` (same reset) + `yarn tests:ios:pod:install` + `yarn tests:ios:run` + `yarn tests:appium:ios`. **Each affected platform.** [Tee](#local-e2e-commands). |
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package io.invertase.googlemobileads
2+
3+
/*
4+
* Copyright (c) 2016-present Invertase Limited & Contributors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this library except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
*/
19+
20+
import com.google.android.gms.ads.AdError
21+
import com.google.android.gms.ads.AdRequest
22+
import com.google.android.gms.ads.AdSize
23+
import org.junit.Assert.assertEquals
24+
import org.junit.Assert.assertFalse
25+
import org.junit.Assert.assertTrue
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
import org.robolectric.RobolectricTestRunner
29+
import org.robolectric.annotation.Config
30+
31+
/**
32+
* Owned mapper/helper coverage for [ReactNativeGoogleMobileAdsCommon].
33+
*
34+
* Does not assert Google Mobile Ads auction, fill, or adapter behavior.
35+
*/
36+
@RunWith(RobolectricTestRunner::class)
37+
@Config(sdk = [34])
38+
class ReactNativeGoogleMobileAdsCommonTest {
39+
@Test
40+
fun isAdManagerUnit_detectsSlashPrefix() {
41+
assertTrue(ReactNativeGoogleMobileAdsCommon.isAdManagerUnit("/1234/unit"))
42+
assertFalse(ReactNativeGoogleMobileAdsCommon.isAdManagerUnit("ca-app-pub-xxx"))
43+
assertFalse(ReactNativeGoogleMobileAdsCommon.isAdManagerUnit(null))
44+
}
45+
46+
@Test
47+
fun stringToAdSize_parsesCustomAndNamedSizes() {
48+
val custom = ReactNativeGoogleMobileAdsCommon.stringToAdSize("320x50")
49+
assertEquals(320, custom.width)
50+
assertEquals(50, custom.height)
51+
52+
assertEquals(AdSize.BANNER, ReactNativeGoogleMobileAdsCommon.stringToAdSize("BANNER"))
53+
assertEquals(AdSize.MEDIUM_RECTANGLE, ReactNativeGoogleMobileAdsCommon.stringToAdSize("medium_rectangle"))
54+
assertEquals(AdSize.LEADERBOARD, ReactNativeGoogleMobileAdsCommon.stringToAdSize("LEADERBOARD"))
55+
assertEquals(AdSize.FLUID, ReactNativeGoogleMobileAdsCommon.stringToAdSize("FLUID"))
56+
assertEquals(AdSize.BANNER, ReactNativeGoogleMobileAdsCommon.stringToAdSize("unknown-size"))
57+
}
58+
59+
@Test
60+
fun getCodeAndMessageFromAdError_mapsOwnedCodes() {
61+
assertMapped(AdRequest.ERROR_CODE_INTERNAL_ERROR, "internal-error")
62+
assertMapped(AdRequest.ERROR_CODE_INVALID_REQUEST, "invalid-request")
63+
assertMapped(AdRequest.ERROR_CODE_NETWORK_ERROR, "network-error")
64+
assertMapped(AdRequest.ERROR_CODE_NO_FILL, "no-fill")
65+
assertMapped(AdRequest.ERROR_CODE_APP_ID_MISSING, "app-id-missing")
66+
assertMapped(AdRequest.ERROR_CODE_MEDIATION_NO_FILL, "mediation-no-fill")
67+
assertMapped(AdRequest.ERROR_CODE_INVALID_AD_STRING, "invalid-ad-string")
68+
assertMapped(AdRequest.ERROR_CODE_REQUEST_ID_MISMATCH, "request-id-mismatch")
69+
assertMapped(999, "unknown")
70+
}
71+
72+
@Test
73+
fun eventConstants_matchOwnedWireNames() {
74+
assertEquals("loaded", ReactNativeGoogleMobileAdsEvent.GOOGLE_MOBILE_ADS_EVENT_LOADED)
75+
assertEquals("error", ReactNativeGoogleMobileAdsEvent.GOOGLE_MOBILE_ADS_EVENT_ERROR)
76+
assertEquals("google_mobile_ads_interstitial_event", ReactNativeGoogleMobileAdsEvent.GOOGLE_MOBILE_ADS_EVENT_INTERSTITIAL)
77+
}
78+
79+
private fun assertMapped(
80+
errorCode: Int,
81+
expectedCode: String,
82+
) {
83+
val error = AdError(errorCode, "detail-$errorCode", "test-domain")
84+
val mapped = ReactNativeGoogleMobileAdsCommon.getCodeAndMessageFromAdError(error)
85+
assertEquals(expectedCode, mapped[0])
86+
assertEquals("detail-$errorCode", mapped[1])
87+
}
88+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package io.invertase.googlemobileads.common
2+
3+
/*
4+
* Copyright (c) 2016-present Invertase Limited & Contributors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this library except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
*/
19+
20+
import android.graphics.Point
21+
import android.graphics.Rect
22+
import org.junit.Assert.assertArrayEquals
23+
import org.junit.Assert.assertEquals
24+
import org.junit.Assert.assertTrue
25+
import org.junit.Test
26+
import org.junit.runner.RunWith
27+
import org.robolectric.RobolectricTestRunner
28+
import org.robolectric.annotation.Config
29+
30+
/**
31+
* Owned helper coverage for [SharedUtils].
32+
*
33+
* Does not assert Google Mobile Ads auction, fill, or adapter behavior.
34+
*/
35+
@RunWith(RobolectricTestRunner::class)
36+
@Config(sdk = [34])
37+
class SharedUtilsTest {
38+
@Test
39+
fun rectToIntArray_handlesNullEmptyAndValues() {
40+
assertArrayEquals(intArrayOf(), SharedUtils.rectToIntArray(null))
41+
assertArrayEquals(intArrayOf(), SharedUtils.rectToIntArray(Rect()))
42+
assertArrayEquals(intArrayOf(1, 2, 3, 4), SharedUtils.rectToIntArray(Rect(1, 2, 3, 4)))
43+
}
44+
45+
@Test
46+
fun pointToIntArray_handlesNullAndValues() {
47+
assertArrayEquals(intArrayOf(), SharedUtils.pointToIntArray(null))
48+
assertArrayEquals(intArrayOf(7, 9), SharedUtils.pointToIntArray(Point(7, 9)))
49+
}
50+
51+
@Test
52+
fun pointsToIntsList_mapsArray() {
53+
val points = arrayOf(Point(1, 2), Point(3, 4))
54+
val mapped = SharedUtils.pointsToIntsList(points)
55+
assertEquals(2, mapped.size)
56+
assertArrayEquals(intArrayOf(1, 2), mapped[0])
57+
assertArrayEquals(intArrayOf(3, 4), mapped[1])
58+
assertTrue(SharedUtils.pointsToIntsList(null).isEmpty())
59+
}
60+
61+
@Test
62+
fun getUri_defaultsMissingSchemeToFile() {
63+
val withScheme = SharedUtils.getUri("https://example.com/a")
64+
assertEquals("https", withScheme.scheme)
65+
assertEquals("example.com", withScheme.host)
66+
67+
val fileUri = SharedUtils.getUri("/tmp/ads-fixture.json")
68+
assertEquals("file", fileUri.scheme)
69+
}
70+
71+
@Test
72+
fun timestampToUTC_formatsEpochSeconds() {
73+
assertEquals("1970-01-01T00:00:00Z", SharedUtils.timestampToUTC(0))
74+
assertEquals("2020-01-01T00:00:00Z", SharedUtils.timestampToUTC(1577836800))
75+
}
76+
}

0 commit comments

Comments
 (0)