Loading...
Loading...
Write Patrol E2E tests using LeanCode's recommended architecture — Modules, System, and ApiClients — with shared test keys. Use when structuring a Patrol test suite, organizing reusable test code into modules, or writing tests in a project that already follows this modular architecture.
npx skill4agent add leancodepl/patrol patrol-test-architecturepatrol-run({ "testFile": "patrol_test/your_test.dart" })patrol-screenshot({ "platform": "android" })patrol-screenshot({ "platform": "ios" })patrol-quit({})patrol-status({})patrol-native-tree({})testAppModulesSystemApiClients$().scrollTo().tap().enterText().waitUntilVisible()import 'package:patrol/patrol.dart';$.platformpatrol testflutter test$.pumpwaitUntilVisiblewaitUntilExistswaitUntilVisibleexpect()waitUntilVisible$.platform.mobile.grantPermissionWhenInUse$.platform.mobile.tapkeyKeyslib/keys.dartkey: keys.feature.widgetNameHomeKeysValueKey<String>widgetbookkeys.dartWidgetKeyswidgetKeys.tilefinal widgetKeys = ds.widgetKeys;import 'package:common_ui/widgets/keys.dart' as ds;key: keys.feature.widgetNamelib/features/home/keys.dart
lib/features/profile/keys.dart
lib/features/auth/keys.dart
lib/common/widgets/keys.dartlib/keys.dart (imports and aggregates all feature keys)// lib/features/home/keys.dart
import 'package:flutter/widgets.dart';
class _HomeKey extends ValueKey<String> {
const _HomeKey(String value) : super('home_$value');
}
class HomeKeys {
final menuIconButton = const _HomeKey('menuIconButton');
_HomeKey navbarItem(String label) => _HomeKey('navbarItem_$label');
}// lib/keys.dart
import 'common/widgets/keys.dart';
import 'features/home/keys.dart';
import 'features/profile/keys.dart';
final keys = Keys();
class Keys {
final home = HomeKeys();
final profile = ProfileKeys();
final widgets = WidgetKeys();
}import 'package:flutter/widgets.dart';
class _ProductPageKey extends ValueKey<String> {
const _ProductPageKey(String value) : super('productPage_$value');
}
class ProductPageKeys {
final menuIconButton = const _ProductPageKey('menuIconButton');
final productImage = const _ProductPageKey('productImage');
final productName = const _ProductPageKey('productName');
}
class _ProductConnectingPageKey extends ValueKey<String> {
const _ProductConnectingPageKey(String value)
: super('productConnectingPage_$value');
}
class ProductConnectingPageKeys {
final productImage = const _ProductConnectingPageKey('productImage');
final productName = const _ProductConnectingPageKey('productName');
}import 'package:flutter/widgets.dart';
class _WidgetKey extends ValueKey<String> {
const _WidgetKey(String value) : super('widget_$value');
}
class WidgetKeys {
final addButton = const _WidgetKey('addButton');
_WidgetKey assetRow(String coinTitle) => _WidgetKey('assetRow_$coinTitle');
_WidgetKey assetSlider(SelectAssetType type) =>
_WidgetKey('assetSlider_$type');
final cancelButton = const _WidgetKey('cancelButton');
final pickCurrencyButton = const _WidgetKey('pickCurrencyButton');
final saveButton = const _WidgetKey('saveButton');
final searchBar = const _WidgetKey('searchBar');
final topBarHeaderMiddleText = const _WidgetKey('topBarHeaderMiddleText');
}
import 'package:flutter/widgets.dart';
final widgetKeys = WidgetBookKeys();
class _WidgetBookKey extends ValueKey<String> {
const _WidgetBookKey(String value) : super('widgetBook_$value');
}
class WidgetBookKeys {
final tile = const _WidgetBookKey('tile');
}
import 'package:common_ui/widgets/keys.dart' as ds;
final keys = Keys();
class Keys {
final widgetKeys = ds.widgetKeys;
}enum SizeDTO {
small,
medium,
large,
}
class PickSizeWidget extends StatelessWidget {
Widget _sizeButton(SizeDTO size) {
return _Button(
key: keys.pickSize.sizeButton(size),
value: size,
currentValue: value,
onPressed: onPressed,
);
}
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sizeButton(SizeDTO.small),
_sizeButton(SizeDTO.medium),
_sizeButton(SizeDTO.large),
].spaced(24),
);
}
} _PickSizeKey sizeButton(SizeDTO size) => _PickSizeKey('sizeButton_${size.name}');import 'package:patrol/patrol.dart';
import 'module.dart';
final class Home extends Module {
Home(super.$);
Future<void> navigateToSettings() async {
await $(keys.home.settingsButton).scrollTo().tap();
}
Future<void> searchForItem(String searchPhrase) async {
await $(keys.home.searchButton).scrollTo().tap();
await $(keys.home.searchInput).enterText(searchPhrase);
await $(keys.home.searchSubmitButton).tap();
}
}final class Modules {
Modules(this._$);
final PatrolIntegrationTester _$;
late final home = Home(_$);
late final auth = Auth(_$);
}final class System extends PlatformAutomator {
System({required super.config});
Future<void> checkIfNativePlayerIsVisible() async {
// Implementation
}
}final class ApiClients {
final backend = BackendClient();
final mailpitClient = MailpitClient();
}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: 'Book of Revelation');
await modules.testament.chooseChapterOfBook(
bookName: 'Book of Revelation',
chapterIndex: 0,
);
await modules.player.expandChapters();
await modules.player.downloadChapter(chapterIndex: 9);
await modules.player.waitUntilDownloaded();
// usage of a method from $.platform 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: 'Book of Revelation');
await modules.player.checkIfChapterIsCorrect(chapterIndex: 0);
await modules.player.playCurrentTrack();
// usage of our method, which calls many methods from $.platform
await system.checkIfNativePlayerIsVisible();
});