Skip to content

Expand Dart test coverage 167 → 292 tests - #100

Merged
JakubMrozek merged 6 commits into
mainfrom
test/dart-coverage
Apr 15, 2026
Merged

Expand Dart test coverage 167 → 292 tests#100
JakubMrozek merged 6 commits into
mainfrom
test/dart-coverage

Conversation

@JakubMrozek

@JakubMrozek JakubMrozek commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Grows Dart-side test coverage from 167 → 292 tests across every class in lib/src/. Matches the depth we already have in sdk-js, sdk-common, sdk-react-native, and sdk-swift. Full suite runs in ~30 s locally.

Coverage by area

Area New tests Files
Models (AdEvent, Character, Message) 24 test/src/models/
Device / App info (7 classes) 38 test/src/device_app_info/
Platform-channel services (6 new) 36 test/src/services/
Widget helpers (AdsProviderData, selectBid, useLastMessages) 25 test/src/widgets/ and widgets/utils/
End-to-end API integration 8 test/src/integration_test.dart

File-by-file audit

Covered directly (this PR)

  • device_app_info/*.dart — all 8 files with a dedicated test
  • models/ad_event.dart, character.dart, message.dart
  • services/sk_ad_network_service.dart, sk_overlay_service.dart, sk_store_product_service.dart, tracking_authorization_service.dart, transparency_consent_framework_service.dart, ad_attribution_kit_service.dart
  • widgets/ads_provider_data.dart
  • widgets/utils/select_bid.dart, use_last_messages.dart

Covered by existing tests

  • models/bid.dart, regulatory.dart
  • services/advertising_id_service.dart, api.dart, http_client.dart, logger.dart
  • utils/extensions.dart, helper_methods.dart, kontext_url_builder.dart, types.dart
  • widgets/ad_format.dart, ads_provider.dart, inline_ad.dart, interstitial_modal.dart, webview_console_error_limiter.dart
  • widgets/utils/use_preload_ads.dart

Intentionally uncovered

  • lib/kontext_flutter_sdk.dart — public barrel file, only re-exports
  • lib/src/main.dart — re-exports
  • lib/src/utils/constants.dart — const values only
  • lib/src/utils/browser_opener.dart — 3-line ChromeSafariBrowser wrapper; not worth the native-mock plumbing
  • lib/src/widgets/kontext_webview.dart — WebView shell, requires platform UI

Integration tests

test/src/integration_test.dart exercises the real ApiHttpClienthttp.Client pipeline end-to-end with only the outermost HTTP client mocked:

  • happy path: POSTs to /preload, correct token + is-disabled headers, body fields (publisherToken, conversationId, userId, enabledPlacementCodes, messages), bids decoded
  • isDisabled: trueKontextso-Is-Disabled: 1
  • skip / no-fill response propagates to PreloadResponse
  • network throw → empty response fallback
  • 5xx statusCode surfaces
  • TCF platform-channel data merges into regulatory without clobbering publisher fields
  • character / variantId / userEmail passthrough
  • sessionId persists across consecutive preloads

A widget-level integration test (AdsProvider + message → triggered preload) was attempted but ended up flaky — AdsProvider's hooks reset HttpClient on mount and the async preload timing isn't reliable under flutter_test. The API-layer integration above covers the same HTTP contract without timing sensitivity.

Running

flutter test ./test

Test plan

  • Locally: 292/292 green in ~30 s
  • File-by-file audit: every lib/src/ file either has a dedicated test or is documented above as intentionally uncovered
  • CI passes

🤖 Generated with Claude Code

JakubMrozek and others added 6 commits April 15, 2026 21:29
AdEvent: enum ↔ string round-trip for every known event, unknown
fallback, payload parsing (top-level code + nested payload for
clicked/viewed/error), null/missing payload handling, malformed
payload swallowed into .unknown, copyWith override semantics, and
the skip-code constants that make up the server contract.

Character: toJson required fields, optional omission, full field
serialisation, additionalProperties merge (including the key-collision
rule where additionalProperties wins).

Message: isUser/isAssistant, toJson uses role.name + ISO-8601 UTC
(converts local DateTime), equality uses (id, role, content) not
createdAt, inequality on each field, identity, non-Message check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers every class under lib/src/device_app_info/ using
TestDefaultBinaryMessengerBinding.setMockMethodCallHandler to stub
platform channels:

- DeviceAudio: empty(), full response decode (volume rounding +
  outputType list), every AudioOutputType mapped, unknown types fall
  back to .other, PlatformException → empty, toJson omits nulls.

- DevicePower: empty(), full response, every BatteryState, unknown
  state → .unknown, integer level widens to double, exception → empty,
  toJson uses enum name.

- DeviceNetwork: empty(), full decode, every NetworkType, unknown →
  null, exception → empty, toJson emits enum name + omits nulls.

- DeviceScreen: empty() zeros + portrait default, init() returns
  non-negative dimensions under the test binding, toJson shape.

- DeviceHardware: empty(), toJson type pass-through, init() does not
  throw under the test binding (DeviceType varies by host platform,
  so assert isA<DeviceType> rather than a specific case).

- OperationSystem: empty(), init() returns well-formed OS info under
  the test binding, exception fallback to empty.

- AppInfo: empty() + safe defaults, init() does not throw under the
  test binding, storeUrl remains null on non-iOS/Android hosts.

- DeviceAppInfo: empty() aggregates sub-empties, toJson shape, toJsonFresh
  invokes screen/audio init, init() is memoised (identity-equal across
  repeated calls).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TransparencyConsentFrameworkService: full channel mocking —
valid shape, gdpr=0, non-0/1 gdprApplies → null, non-int or
empty/non-string tcString → null, null-response handling, exception
fallback.

SKOverlayService: uses the injectable `isIOS` hook for full coverage —
non-iOS short-circuit, empty-itunesItem short-circuit, forwards skan/
position/dismissible, false-result passthrough, UNSUPPORTED_IOS
swallowed, any PlatformException swallowed, dismiss happy path + non-iOS
short-circuit + error path.

SKStoreProductService: non-iOS short-circuit, channel forwarding,
native false/throw cases, dismiss paths.

TrackingAuthorizationService: notSupported on non-iOS, enum index
mapping for each TrackingStatus, value-list coverage.

SKAdNetwork and AdAttributionKit: non-iOS short-circuit for every
public method (initImpression/startImpression/endImpression/dispose,
and initImpression/setAttributionFrame/handleTap/beginView/endView/
dispose respectively). The iOS code path is guarded by Platform.isIOS
which cannot be flipped from a Dart test; that coverage lives in the
respective native tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
selectBid: placement-not-enabled, code-not-matched, happy path,
relevantAssistantMessageId precedence over lastAssistantMessageId,
not-ready → null, mismatched messageId → null, afterUser branch
(ready/not-ready/mismatch), first-bid-wins when duplicate codes.

useLastMessages hook: empty list resets every setter; assistant-last
emits ids and readyForStreaming=true; user-last sets
readyForStreaming=false; relevantAssistantMessageId resets on a new
user message id; no reset when the id is unchanged.

AdsProviderData: .of returns the nearest ancestor or null;
updateShouldNotify returns false on equal values and true on any
of the tracked field changes (adServerUrl, isDisabled, the two
readyForStreaming flags, each messageId, messages/bids/placements
list, and otherParams map); deep-equal messages list and deep-equal
otherParams do not retrigger.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drives the full Api → HttpClient → http.Client pipeline end-to-end
with only the outermost http.Client mocked. This proves the wire
contract that publishers rely on.

- happy path: POSTs to /preload, publisher-token and is-disabled
  headers, body fields (publisherToken, conversationId, userId,
  enabledPlacementCodes, messages), bids decoded
- isDisabled=true sends Kontextso-Is-Disabled: 1
- skip:true response propagates to PreloadResponse.skip/skipCode
- network throw falls back to an empty PreloadResponse
- 5xx surfaces statusCode in the response
- TCF platform-channel data is merged into regulatory (gdpr +
  gdprConsent) without clobbering publisher-provided fields (coppa)
- character/variantId/userEmail passthrough
- sessionId persists across consecutive preloads when passed in

Widget-level integration (AdsProvider + messages → triggered preload)
was attempted but ended up flaky because AdsProvider is a HookWidget
that resets HttpClient on mount; reliable simulation of the
hook-driven rebuild + async preload timing in flutter_test is fragile.
The API-layer integration above covers the same HTTP contract with no
timing sensitivity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- integration_test.dart: drop unused bid.dart import
- message_test.dart: ignore unrelated_type_equality_checks on the
  "Message != non-Message" assertion (the whole point of the test)
- select_bid_test.dart: flip makeData defaults to false/[] so every
  test explicitly opts in to the flag it exercises — removes 7
  avoid_redundant_argument_values warnings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JakubMrozek
JakubMrozek merged commit c004430 into main Apr 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant