From de7faa9a4bd2885723b478e1e1bc49af3c7d2ff5 Mon Sep 17 00:00:00 2001 From: wzslr321 Date: Mon, 13 Jul 2026 11:07:24 +0200 Subject: [PATCH] Source flutter-patrol from leancodepl/patrol The Patrol repo owns all Patrol AI support now. The marketplace entry points at github:leancodepl/patrol with strict: false and exposes skills/patrol-write-test and skills/patrol-test-architecture. The local plugin directory is deleted, no copy of Patrol content remains here. Validator parses object sources, checks the github repo slug, and skips local structure checks for external plugins. Co-Authored-By: Claude Fable 5 --- .claude-plugin/marketplace.json | 11 +- AGENTS.md | 17 ++ README.md | 4 +- internal/pluginvalidation/marketplace.go | 7 +- internal/pluginvalidation/types.go | 33 ++- plugins/flutter-marionette/README.md | 4 +- .../flutter-patrol/.claude-plugin/plugin.json | 20 -- plugins/flutter-patrol/README.md | 22 -- .../skills/flutter-patrol-usage/SKILL.md | 40 ---- .../references/patrol-keys.md | 221 ------------------ .../references/patrol-tests.md | 172 -------------- 11 files changed, 64 insertions(+), 487 deletions(-) delete mode 100644 plugins/flutter-patrol/.claude-plugin/plugin.json delete mode 100644 plugins/flutter-patrol/README.md delete mode 100644 plugins/flutter-patrol/skills/flutter-patrol-usage/SKILL.md delete mode 100644 plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-keys.md delete mode 100644 plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-tests.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8ccadea..8ce00f7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -108,10 +108,15 @@ }, { "name": "flutter-patrol", - "source": "./plugins/flutter-patrol", - "description": "LeanCode Patrol E2E testing guidance (Modules/System/ApiClients, keys conventions) plus Patrol MCP setup for AI-assisted test development.", + "source": { + "source": "github", + "repo": "leancodepl/patrol" + }, + "strict": false, + "description": "Patrol E2E testing skills (write-test workflow and Modules/System/ApiClients architecture with keys conventions), sourced directly from the Patrol repository.", "skills": [ - "./skills/flutter-patrol-usage" + "./skills/patrol-write-test", + "./skills/patrol-test-architecture" ] }, { diff --git a/AGENTS.md b/AGENTS.md index bedb1b3..714833b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,23 @@ LeanCode's shared AI plugins for Claude Code, focused on Flutter development. Claude Code also documents support for per-plugin `agents/`, `commands/`, and `hooks/` directories. No plugin in this repo currently uses them — treat them as not-yet-exercised. +## Externally sourced plugins + +A plugin whose content is owned by another repository is registered in `.claude-plugin/marketplace.json` with an object source and `strict: false`: + +```json +{ + "name": "flutter-patrol", + "source": { "source": "github", "repo": "leancodepl/patrol" }, + "strict": false, + "skills": ["./skills/patrol-write-test", "./skills/patrol-test-architecture"] +} +``` + +The external repo is the single source of truth — there is no `plugins//` directory and no copy of its content here; do not vendor one. The validator skips external entries (no local directory to check); the `claude plugin validate` CI step covers the entry's schema. + +Currently external: `flutter-patrol` (from [leancodepl/patrol](https://github.com/leancodepl/patrol), which owns all Patrol AI support). + ## Working conventions - Keep plugins self-contained and independently installable. diff --git a/README.md b/README.md index 9733f55..87af138 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ To have Claude Code prompt collaborators to install the marketplace automaticall Most plugins are pure rules and skills with no setup. A few need one-time tooling or MCP setup — finish it from the plugin's `README.md`: -- [`flutter-patrol`](plugins/flutter-patrol/) - Patrol CLI and Patrol MCP +- [`flutter-patrol`](https://github.com/leancodepl/patrol) - Patrol CLI and Patrol MCP - [`flutter-marionette`](plugins/flutter-marionette/) - Marionette MCP and app-side binding ## Available plugins @@ -80,7 +80,7 @@ Most plugins are pure rules and skills with no setup. A few need one-time toolin ### UI and verification - [`flutter-ui`](plugins/flutter-ui/) - design-system-driven UI, loading/error patterns, localized presentation text, and UI implementation checklists -- [`flutter-patrol`](plugins/flutter-patrol/) - Patrol test architecture, key conventions, and Patrol MCP workflow for AI-assisted E2E work +- [`flutter-patrol`](https://github.com/leancodepl/patrol) - Patrol E2E test skills (write-test workflow, test architecture, key conventions, Patrol MCP), sourced directly from the Patrol repository's `skills/` — the single source of truth for Patrol AI support - [`flutter-marionette`](plugins/flutter-marionette/) - runtime interaction with a live debug app through Marionette MCP for exploration, smoke checks, and UI debugging - [`flutter-read-logs`](plugins/flutter-read-logs/) - read the running app's latest `flutter run` logs as on-demand context via `/read-logs` diff --git a/internal/pluginvalidation/marketplace.go b/internal/pluginvalidation/marketplace.go index db57055..1cf8a83 100644 --- a/internal/pluginvalidation/marketplace.go +++ b/internal/pluginvalidation/marketplace.go @@ -65,14 +65,17 @@ func (v *validator) validateMarketplaceEntries(where string, entries []marketpla seen[entry.Name] = i } } - v.requireNonEmpty(where, fmt.Sprintf("plugins[%d].source", i), entry.Source) + if !entry.External { + v.requireNonEmpty(where, fmt.Sprintf("plugins[%d].source", i), entry.Source) + } } } func (v *validator) registeredPlugins(entries []marketplacePlugin) map[string]struct{} { registered := map[string]struct{}{} for _, entry := range entries { - if entry.Name != "" { + // External plugins live in another repository: nothing local to validate. + if entry.Name != "" && !entry.External { registered[entry.Name] = struct{}{} } } diff --git a/internal/pluginvalidation/types.go b/internal/pluginvalidation/types.go index 3a12095..fa0b19b 100644 --- a/internal/pluginvalidation/types.go +++ b/internal/pluginvalidation/types.go @@ -1,14 +1,41 @@ package pluginvalidation +import "encoding/json" + type marketplace struct { Metadata map[string]any `json:"metadata"` Plugins []marketplacePlugin `json:"plugins"` } type marketplacePlugin struct { - Name string `json:"name"` - Source string `json:"source"` - Description string `json:"description"` + Name string + Source string + External bool + Description string +} + +// UnmarshalJSON accepts both source forms: a local path string and an object +// pointing at an external repository ({"source": "github", ...}). External +// plugins have no local directory, so the validator skips them. +func (p *marketplacePlugin) UnmarshalJSON(data []byte) error { + var raw struct { + Name string `json:"name"` + Source json.RawMessage `json:"source"` + Description string `json:"description"` + } + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + p.Name = raw.Name + p.Description = raw.Description + if len(raw.Source) == 0 { + return nil + } + if raw.Source[0] == '{' { + p.External = true + return nil + } + return json.Unmarshal(raw.Source, &p.Source) } type pluginManifest struct { diff --git a/plugins/flutter-marionette/README.md b/plugins/flutter-marionette/README.md index b6d88d4..359244e 100644 --- a/plugins/flutter-marionette/README.md +++ b/plugins/flutter-marionette/README.md @@ -11,7 +11,7 @@ LeanCode Flutter plugin for [Marionette MCP](https://github.com/leancodepl/mario ## Marionette vs Patrol -| | Marionette (this plugin) | Patrol ([`flutter-patrol`](../flutter-patrol/)) | +| | Marionette (this plugin) | Patrol ([`flutter-patrol`](https://github.com/leancodepl/patrol)) | |---|---|---| | Purpose | Runtime exploration, smoke verification | Deterministic E2E test suites | | Runs against | Live `flutter run` debug session | `patrol develop` / `patrol test` | @@ -124,5 +124,5 @@ Marionette relies on Flutter's VM Service and is intended for a live `flutter ru ## Related plugins -- [`flutter-patrol`](../flutter-patrol/) — deterministic E2E testing with Patrol MCP +- [`flutter-patrol`](https://github.com/leancodepl/patrol) — deterministic E2E testing with Patrol MCP - [`flutter-ui`](../flutter-ui/) — design-system guidance (relevant when configuring `isInteractiveWidget` for custom widgets) diff --git a/plugins/flutter-patrol/.claude-plugin/plugin.json b/plugins/flutter-patrol/.claude-plugin/plugin.json deleted file mode 100644 index ffea51b..0000000 --- a/plugins/flutter-patrol/.claude-plugin/plugin.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", - "name": "flutter-patrol", - "displayName": "Flutter Patrol", - "description": "LeanCode Patrol E2E testing guidance (Modules/System/ApiClients, keys conventions) plus Patrol MCP setup for AI-assisted test development.", - "version": "0.1.0", - "author": { - "name": "LeanCode" - }, - "homepage": "https://github.com/leancodepl/ai-plugins", - "repository": "https://github.com/leancodepl/ai-plugins", - "keywords": [ - "flutter", - "patrol", - "e2e-testing", - "automation", - "mcp" - ], - "skills": "./skills/" -} diff --git a/plugins/flutter-patrol/README.md b/plugins/flutter-patrol/README.md deleted file mode 100644 index cea5166..0000000 --- a/plugins/flutter-patrol/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# flutter-patrol - -LeanCode Flutter plugin for Patrol E2E testing. - -Contains the Patrol rules and workflow assets: - -- `skills/flutter-patrol-usage/references/patrol-tests.md` - Patrol test structure, API rules, native dialogs, and MCP workflow -- `skills/flutter-patrol-usage/references/patrol-keys.md` - key placement and naming conventions -- `skills/flutter-patrol-usage/SKILL.md` - entry point for this plugin - -## Example usage - -- `/flutter-patrol-usage` - get a short explanation of what this plugin does and which asset to use next - -## What this plugin is NOT about - -General cubit/bloc logic lives in [`flutter-bloc`](../flutter-bloc/). Route structure lives in [`flutter-navigation`](../flutter-navigation/). - -## Related plugins - -- [`flutter-bloc`](../flutter-bloc/) - cubit-driven flows exercised by Patrol tests -- [`flutter-navigation`](../flutter-navigation/) - navigation flows exercised by Patrol tests diff --git a/plugins/flutter-patrol/skills/flutter-patrol-usage/SKILL.md b/plugins/flutter-patrol/skills/flutter-patrol-usage/SKILL.md deleted file mode 100644 index fc65ee4..0000000 --- a/plugins/flutter-patrol/skills/flutter-patrol-usage/SKILL.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: flutter-patrol-usage -description: Explain what the `flutter-patrol` plugin does and how to use it. Use when the user invokes `/flutter-patrol-usage`, asks what this plugin covers, or needs help with Patrol E2E tests, test keys, native automation, or Patrol MCP. ---- - -# Patrol Usage - -## How to respond - -- If the user invoked this skill without a concrete task, start by explaining what this plugin does and when Patrol is the right tool. -- Show how to use it through concrete testing tasks this plugin can handle. -- Point to the relevant rule for tests, keys, or Patrol MCP usage. -- If the user already gave a concrete Patrol task, briefly explain why this plugin fits and then do the work. -- Do not reply with filler like "skill loaded", "ready for the task", or "what would you like to do?" before explaining the plugin. - -## What this plugin does - -- Covers LeanCode Patrol conventions for E2E UI tests. -- Guides Patrol test structure, assertions, key naming, custom finders, and native automation boundaries. -- Helps with Patrol MCP usage for agent-driven test runs and debugging. - -## How to use it - -- Ask to create or refactor a Patrol E2E test. -- Ask to add or review test keys for a feature flow. -- Ask how to handle native dialogs, permissions, notifications, or other platform UI. -- Ask how to run or debug a test through Patrol MCP. - -## Example requests - -- "Create a Patrol test for the login flow." -- "Review this feature for missing Patrol keys." -- "Help me run this test through Patrol MCP." - -## Reach for these assets - -- `references/patrol-tests.md` - test structure and Patrol API conventions. -- `references/patrol-keys.md` - key naming and composition rules. -- Related plugin: `flutter-bloc` - state-management conventions for cubit-driven flows under test. -- Related plugin: `flutter-navigation` - route structure and navigation flows exercised by Patrol tests. diff --git a/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-keys.md b/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-keys.md deleted file mode 100644 index ed641b7..0000000 --- a/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-keys.md +++ /dev/null @@ -1,221 +0,0 @@ -# Patrol Keys - -## Core rules - -- Assign a key ONLY to widgets involved in testing. -- ONLY add the `key` parameter to existing widgets — never modify widget signatures or refactor existing code structure. -- Never hardcode keys in the app — always use keys from the `keys.dart` file. -- Never create a key that is not assigned to a widget. -- Always make sure each key value is unique. -- Sort keys alphabetically. -- Add keys as the **first parameter** to the widget constructor. - -## File structure - -- Create `keys.dart` in the feature/widget directory — NOT in the main `lib/` directory. -- Maximum one `keys.dart` per feature directory (the file can contain multiple classes). -- The main `Keys` class in `lib/keys.dart` should only import and aggregate feature-specific keys. -- Create new `keys.dart` files as needed. Add imports for them in the main aggregator. - -Example layout: - -``` -lib/features/home/keys.dart -lib/features/profile/keys.dart -lib/features/auth/keys.dart -lib/common/widgets/keys.dart -lib/keys.dart ← aggregator only -``` - -## Key usage pattern - -Always assign keys using this exact pattern: - -```dart -key: keys.feature.widgetName -``` - -(except when using parameterized keys — see below) - -## Grouping - -Group related keys in a class named after the screen (e.g. `HomeKeys`). Use a private `ValueKey` subclass to prefix all key values with the page or widget name. - -```dart -// lib/features/home/keys.dart -import 'package:flutter/widgets.dart'; - -class _HomeKey extends ValueKey { - const _HomeKey(String value) : super('home_$value'); -} - -class HomeKeys { - final menuIconButton = const _HomeKey('menuIconButton'); - _HomeKey navbarItem(String label) => _HomeKey('navbarItem_$label'); -} -``` - -## Main aggregator - -```dart -// lib/keys.dart -import 'features/home/keys.dart'; -import 'features/profile/keys.dart'; -import 'common/widgets/keys.dart'; - -final keys = Keys(); - -class Keys { - final home = HomeKeys(); - final profile = ProfileKeys(); - final widgets = WidgetKeys(); -} -``` - -## Grouping multiple screens per feature - -```dart -// lib/features/product/keys.dart -import 'package:flutter/widgets.dart'; - -class _ProductPageKey extends ValueKey { - const _ProductPageKey(String value) : super('productPage_$value'); -} - -class ProductPageKeys { - final menuIconButton = _ProductPageKey('menuIconButton'); - final productImage = _ProductPageKey('productImage'); - final productName = _ProductPageKey('productName'); -} - -class _ProductConnectingPageKey extends ValueKey { - const _ProductConnectingPageKey(String value) - : super('productConnectingPage_$value'); -} - -class ProductConnectingPageKeys { - final productName = _ProductConnectingPageKey('productName'); - final productImage = _ProductConnectingPageKey('productImage'); -} -``` - -## Common widgets - -For common widgets used across features, store keys in `WidgetKeys` placed in the directory of the common widget: - -```dart -// lib/common/widgets/keys.dart -import 'package:flutter/widgets.dart'; - -class _WidgetKey extends ValueKey { - const _WidgetKey(String value) : super('widget_$value'); -} - -class WidgetKeys { - final addButton = const _WidgetKey('addButton'); - final searchBar = const _WidgetKey('searchBar'); - final saveButton = const _WidgetKey('saveButton'); - final cancelButton = const _WidgetKey('cancelButton'); - final topBarHeaderMiddleText = const _WidgetKey('topBarHeaderMiddleText'); - final pickCurrencyButton = const _WidgetKey('pickCurrencyButton'); - _WidgetKey assetSlider(SelectAssetType type) => - _WidgetKey('assetSlider_$type'); - ValueKey assetRow(String coinTitle) => - _WidgetKey('assetRow_${coinTitle}'); -} -``` - -## External packages - -For widgets in a separate package (e.g. widgetbook), create a `keys.dart` in that package: - -```dart -// widgetbook/keys.dart -import 'package:flutter/widgets.dart'; - -final widgetBook = WidgetBookKeys(); - -class _WidgetBookKey extends ValueKey { - const _WidgetBookKey(String value) : super('widgetBook_$value'); -} - -class WidgetBookKeys { - final tile = const _WidgetBookKey('tile'); -} -``` - -Import to the main aggregator: - -```dart -import 'package:common_ui/widgets/keys.dart' as ds; - -final keys = Keys(); - -class Keys { - final designSystem = ds.dsKeys; - // ... -} -``` - -## Parameterized keys - -Use parameterized keys when: - -- Widgets are generated from dynamic data. -- Widgets are generated from a DTO or enum. -- Number of widgets is variable or large. -- Widgets are generated in loops or from lists. - -Use individual keys when: - -- Widgets are hardcoded and known at compile time. -- Widgets have distinct, meaningful names. - -### Parameterized key rules - -- ALWAYS prefer using enums or DTOs as the parameter if they already exist. -- Use existing widget properties for parameterized keys. -- NEVER assign a parameterized key in the app but use fixed values in the keys file (and vice versa). -- NEVER create helper methods — use parameterized keys instead. -- When widgets are generated from existing enums or DTOs, always use parameterized keys with those enum/DTO values. - -### Example - -```dart -// app widget -enum SizeDTO { small, medium, large } - -Widget _sizeButton(SizeDTO size) { - return _Button( - key: keys.pickSize.sizeButton(size), - value: size, - currentValue: value, - onPressed: onPressed, - ); -} - -@override -Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sizeButton(SizeDTO.small), - _sizeButton(SizeDTO.medium), - _sizeButton(SizeDTO.large), - ].spaced(24), - ); -} -``` - -```dart -// keys.dart -_PickSizeKey sizeButton(SizeDTO size) => - _PickSizeKey('sizeButton_${size.name}'); -``` - -## Steps to assign a key to a widget - -1. Identify the widget that is needed for testing. -2. Create the key assignment: `key: keys.feature.widgetName`. -3. Define the key in the feature's `keys.dart` file. -4. Verify the key is assigned to the widget. diff --git a/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-tests.md b/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-tests.md deleted file mode 100644 index 8326032..0000000 --- a/plugins/flutter-patrol/skills/flutter-patrol-usage/references/patrol-tests.md +++ /dev/null @@ -1,172 +0,0 @@ -# Patrol Tests - -Patrol is LeanCode's Flutter-first E2E UI testing framework. It extends `integration_test` with native automation (`$.platform`) and a custom finder system. - -`patrol_mcp` (released 2026-03-31) exposes a Dart-based MCP server that wraps `patrol develop`, letting AI agents run tests, capture screenshots, inspect the native UI tree, and iterate on failing tests without manual handoff. - -## Order of actions when writing new tests - -1. Read provided test steps. -2. Inspect existing modules for functions that can be reused. -3. Consider whether an existing function can be adjusted to match both its existing usage and the new test. -4. Assign test keys to required elements if they are not assigned yet (see `patrol-keys`). -5. Start writing the test: reuse existing modules/functions and put new test steps in a new test file. -6. Write Patrol actions directly in the test file — do not create new methods in modules yet. -7. After the test passes, reorganize the new code into existing or new modules. -8. Recheck the test after refactoring. - -## Patrol MCP usage - -When the `patrol_mcp` MCP server is configured, use these tools instead of manual `patrol develop` runs: - -- `patrol-run({ "testFile": "patrol_test/your_test.dart" })` — run a test and wait for completion. If no session is running, starts a new one; if a session is running, restarts the current test. -- `patrol-screenshot({ "platform": "android" })` or `patrol-screenshot({ "platform": "ios" })` — capture a screenshot for debugging test failures. -- `patrol-quit({})` — gracefully quit the session. -- `patrol-status({})` — check current session status and recent output. -- `patrol-native-tree({})` — fetch the current native UI tree hierarchy (used for writing native interactions or interacting with apps other than the app under test). - -**Known limitation:** on iOS, `patrol develop` tends to time out after 360 seconds. - -See the plugin README for how to install `patrol_mcp` and register the MCP launcher. - -## Test architecture - -### Structure - -- Use the custom `testApp` wrapper for all tests. -- Each test file should contain **one** test. -- Organize tests using the `Modules`, `System`, and `ApiClients` pattern: - - **Modules** — one per user-perspective feature (e.g. `Auth`, `Home`, `Downloads`, `Player`). - - **System** — class for native interactions via `$.platform` that are not part of your app (e.g. enabling airplane mode to test offline mode). Do not put app-specific methods here. - - **ApiClients** — aggregates API clients used during testing (e.g. backend test client, mail server client, third-party services). - -### Method organization in modules - -- Do not write comments in methods — use descriptive method names instead. -- Split long methods when they become hard to read and represent multiple nameable logical steps. -- If a series of steps is reused across many tests as a whole (e.g. a long onboarding flow), create a wrapper method in the module that calls private methods for each step. - -## Patrol API rules - -- Any file that directly uses Patrol APIs (`$()`, `.scrollTo()`, `.tap()`, `.enterText()`, `.waitUntilVisible()`, etc.) must `import 'package:patrol/patrol.dart';`. -- ALWAYS inspect the Patrol API before implementing a test action: - - Search the codebase for existing Patrol API usage patterns. - - Check `$.platform` APIs for the specific action. - - If a method is not found in the codebase, check: https://patrol.leancode.co/. - - Only implement after confirming the correct API method. -- ALWAYS inspect `$.platform` methods before implementing native test actions. -- Do not use the `flutter_test` package. Use only Patrol API. -- Only run tests with the Patrol MCP server (for a single test) or `patrol test` (for all tests). Never use the `flutter test` command. -- Do not write `patrolSetUp` or `patrolTearDown` methods yourself. - -## Action rules - -- Do not use `$.pump`, `waitUntilVisible`, or `waitUntilExists` (or other wait methods) before or after `tap`, `scrollTo`, or `enterText`. Patrol handles waiting automatically. Use wait methods only at the end of the test for assertions. -- To find widgets, use only keys. -- Do not write `try/catch` blocks unless absolutely necessary. -- After writing a test, check that it works by running it with the MCP server; fix it if it fails. -- If a test fails because an element was not found, check whether it needs to be scrolled to in the app code and adjust the test if needed. - -## Assertion rules - -- Do not write assertions after actions. Write them at the end of the test. -- Prefer `waitUntilVisible` as the final assertion. -- Use `expect()` for assertions only when `waitUntilVisible` is not enough. - -## Native dialog handling - -- ALWAYS handle native dialogs that appear during the flow: - - Handle dialogs immediately after the action that triggers them. - - For native permissions, prefer `$.platform.mobile.grantPermissionWhenInUse` over `$.platform.mobile.tap`. - -## Code examples - -### Module - -```dart -// patrol_test/modules/home.dart -import 'package:patrol/patrol.dart'; -import 'module.dart'; - -final class Home extends Module { - Home(super.$); - - Future navigateToSettings() async { - await $(keys.home.settingsButton).scrollTo().tap(); - } - - Future searchForItem(String searchPhrase) async { - await $(keys.home.searchButton).scrollTo().tap(); - await $(keys.home.searchInput).enterText(searchPhrase); - await $(keys.home.searchSubmitButton).tap(); - } -} -``` - -### Modules aggregator - -```dart -// patrol_test/modules/modules.dart -final class Modules { - Modules(this._$); - final PatrolIntegrationTester _$; - - late final home = Home(_$); - late final auth = Auth(_$); -} -``` - -### System class - -```dart -// patrol_test/modules/system.dart -final class System extends PlatformAutomator { - System({required super.config}); - - Future checkIfNativePlayerIsVisible() async { - // Implementation - } -} -``` - -### ApiClients class - -```dart -// patrol_test/modules/api_clients.dart -final class ApiClients { - final backend = BackendClient(); - final mailpitClient = MailpitClient(); -} -``` - -### Complete test - -```dart -testApp('Download a chapter and play it offline', ($, modules, system, apiClients) async { - await modules.auth.getAuthToken(); - await apiClients.backend.addFavourites(); - await openApp($); - await modules.home.goToOldTestament(); - await modules.testament.expandBook(bookName: 'Ksiega Rodzaju'); - await modules.testament.chooseChapterOfBook( - bookName: 'Ksiega Rodzaju', - chapterIndex: 0, - ); - await modules.player.expandChapters(); - await modules.player.downloadChapter(chapterIndex: 9); - await modules.player.waitUntilDownloaded(); - // equivalent of await $.platform.mobile.enableAirplaneMode(); - await system.enableAirplaneMode(); - await modules.player.rollDownChapters(); - await modules.player.closeChapterPlayer(); - await modules.testament.closeTestament(); - await modules.bottomNavigation.goToLibrary(); - await modules.library.goToDownloads(); - await modules.downloads.expandOldTestament(); - await modules.downloads.goToBook(bookName: 'Ksiega Rodzaju'); - await modules.player.checkIfChapterIsCorrect(chapterIndex: 0); - await modules.player.playCurrentTrack(); - // uses a wrapper method that calls multiple $.platform methods - await system.checkIfNativePlayerIsVisible(); - }); -```