Webinar: Mastering Patrol & AI: Next-Level E2E Testing. Register now

Extension packages

Patrol can be extended with optional companion packages (for example, SDK-specific packages such as accessibility, payments, biometrics, etc.) without adding SDK logic to core patrol.

This page explains the recommended pattern for building such a package.

Architecture in one minute

  1. Core Patrol starts the native automation server on :8081.
  2. Core Patrol discovers extension implementations at startup:
    • Android via ServiceLoader (SPI file in META-INF/services/...)
    • iOS via an explicit registry populated at module load time (+load calls PatrolRegisterServerExtensionClass)
  3. Extension package registers extra POST endpoints (for example /mySdkScan) on the same server.
  4. Dart API in the extension package calls those endpoints over HTTP (same host/port as $.platform.mobile.*).

The only core dependency from extension Dart code is patrolNativeServerUri.

What belongs where

AreaCore patrolExtension package
Route discovery and mounting
SDK-specific routes and handlers
Third-party SDK dependencies
Package-owned schema (schema/*.dart)
$.mySdk Dart extension API

Step-by-step: create a new package

1) Create Flutter plugin package

Create a package with Flutter plugin structure (for example patrol_my_sdk).

In pubspec.yaml, keep it as a test-time dependency in consumer apps:

consumer app pubspec.yaml
dev_dependencies:
  patrol: ^<version>
  patrol_my_sdk: ^<version>

2) Define package schema

Create a package-local schema file, for example schema/my_sdk.dart.

schema/my_sdk.dart
class MySdkScanRequest {
  late bool upload;
}

class MySdkScanResponse {
  late int violations;
}

abstract class MySdkAutomator<IOSServer, AndroidServer, DartClient> {
  MySdkScanResponse mySdkScan(MySdkScanRequest request);
}

Use unique endpoint prefixes (mySdk*) to avoid route collisions.

Run patrol_gen for the package schema to produce:

  • Dart client (DartClient enabled)
  • Android server contracts/routes (AndroidServer enabled)
  • iOS server contracts/routes (IOSServer enabled)

You can hand-write the HTTP client in a POC, but production packages should prefer codegen so endpoint names and payloads stay synchronized.

4) Build Dart API ($.mySdk)

Expose an extension on PatrolIntegrationTester:

lib/src/patrol_my_sdk_extension.dart
extension PatrolMySdkExtension on PatrolIntegrationTester {
  MySdkAutomator get mySdk => MySdkAutomator(this);
}

Inside your package automator, initialize client with:

patrolNativeServerUri

This ensures the same host/port resolution as core Patrol (PATROL_HOST / PATROL_TEST_SERVER_PORT).

5) Implement Android native extension

Implement Patrol's extension interface and return routes:

android/src/main/kotlin/.../MySdkServerExtension.kt
class MySdkServerExtension : PatrolServerExtension {
    override val name: String = "patrol_my_sdk"

    override fun routes(): RoutingHttpHandler = routes(
        "mySdkScan" bind POST to { req ->
            // parse JSON, call SDK, return response
            Response(OK).body(...)
        },
    )
}

Register with SPI file:

android/src/main/resources/META-INF/services/pl.leancode.patrol.PatrolServerExtension
com.example.patrol_my_sdk.MySdkServerExtension

6) Implement iOS native extension

Create class conforming to PatrolServerExtension and register endpoints via PatrolRouteRegistrar.

darwin/patrol_my_sdk/Sources/PatrolMySdk/MySdkServerExtension.swift
@objc(MySdkServerExtension)
public class MySdkServerExtension: NSObject, PatrolServerExtension {
  public override required init() { super.init() }
  public var name: String { "patrol_my_sdk" }

  public func register(on registrar: PatrolRouteRegistrar) {
    registrar.post("mySdkScan") { body in
      // decode request, call SDK, encode response
      return Data()
    }
  }
}

Register the class at module load time so Patrol can discover it without scanning the full Objective-C runtime (which is unsafe in large UITest binaries):

darwin/patrol_my_sdk/Sources/PatrolMySdk/MySdkServerExtensionRegistration.m
#import <Foundation/Foundation.h>
#import <patrol/PatrolExtensionRegistry.h>

@interface MySdkExtensionLoader : NSObject
@end

@implementation MySdkExtensionLoader

+ (void)load {
  Class extensionClass = NSClassFromString(@"MySdkServerExtension");
  if (extensionClass == Nil) {
    return;
  }
  PatrolRegisterServerExtensionClass(extensionClass);
}

@end

Do not call registerExtensionClass: via performSelector: — Swift class methods are not reliably invokable that way from ObjC.

Patrol invokes extension route handlers on the HTTP server's worker threads, not on the main thread. If your handler calls XCTest or XCUI APIs (for example XCUIApplication()), dispatch that work to the main thread first — core Patrol's IOSAutomator does the same via DispatchQueue.main.sync. Calling XCTest from a background thread crashes with Must be called on the main thread.

7) Consumer app wiring

Android

The extension must be available on the instrumentation (androidTest) classpath:

android/app/build.gradle.kts
dependencies {
  androidTestImplementation(project(":patrol_my_sdk"))
}

In some repository layouts (for example local monorepo path deps), you may also need to map Gradle project path in android/settings.gradle(.kts).

iOS

Use the standard Patrol iOS setup:

  • RunnerUITests target configured
  • FlutterGeneratedPluginSwiftPackage linked to RunnerUITests (SPM), or inherit! :complete for RunnerUITests in Podfile (CocoaPods)

Then the extension plugin is pulled into the UITest target with the rest of Flutter plugins.

For a full reference implementation, see patrol_axe.

On this page