patrol-test-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOrder of actions when writing new tests
编写新测试的操作顺序
- Read the key files directly (modules aggregator, test wrapper, main keys.dart, and the specific app feature files for the scenario). These are known paths — don't search for them, open them directly
- Inspect existing modules for functions that can be reused
- Also think if one of existing functions can be adjusted to match its existing usage and new test
- Assign test keys to required elements if they are not assigned yet
- Start writing test: reuse existing modules/functions + put new test steps in new test file
- Write patrol actions directly in the test file, do not create new methods in modules
- Run the test frequently during development — don't wait until the full test is written. Run after completing each logical group of steps to catch failures early
- After full test passes, reorganize new code into existing/new modules. This is mandatory — the test file must only call module methods, not use Patrol APIs directly
- Rerun the test after reorganizing to confirm it still passes
- 直接读取关键文件(模块聚合器、测试包装器、主keys.dart文件,以及对应场景的特定应用功能文件)。这些是已知路径——无需搜索,直接打开即可
- 检查现有模块,寻找可复用的函数
- 同时思考是否可以调整某个现有函数,使其适配现有用法和新测试需求
- 若所需元素尚未分配测试键,为其分配测试键
- 开始编写测试:复用现有模块/函数 + 将新测试步骤写入新测试文件
- 直接在测试文件中编写Patrol操作,不要在模块中创建新方法
- 在开发过程中频繁运行测试——不要等到完整测试编写完成后再运行。完成每组逻辑步骤后就运行一次,尽早发现失败问题
- 完整测试通过后,将新代码重组到现有/新模块中。这是强制要求——测试文件必须仅调用模块方法,不得直接使用Patrol API
- 重组后重新运行测试,确认测试仍能通过
Patrol MCP Usage
Patrol MCP 使用方法
When working with Patrol tests:
- Use to run tests and wait for completion
patrol-run({ "testFile": "patrol_test/your_test.dart" }) - If no session running: starts new session with specified test file
- If session already running: automatically restarts current tests
- Use or
patrol-screenshot({ "platform": "android" })to capture screenshots for debugging test failurespatrol-screenshot({ "platform": "ios" }) - Use to quit the session gracefully
patrol-quit({}) - Use to check current status and recent output
patrol-status({}) - Use to fetch the current native UI tree hierarchy for writing native interactions and interactions with apps other than the app under test.
patrol-native-tree({})
使用Patrol测试时:
- 使用 运行测试并等待完成
patrol-run({ "testFile": "patrol_test/your_test.dart" }) - 若没有会话在运行:启动包含指定测试文件的新会话
- 若已有会话在运行:自动重启当前测试
- 使用 或
patrol-screenshot({ "platform": "android" })捕获截图,用于调试测试失败问题patrol-screenshot({ "platform": "ios" }) - 使用 优雅退出会话
patrol-quit({}) - 使用 检查当前状态和最近输出
patrol-status({}) - 使用 获取当前原生UI树层级,用于编写原生交互以及与被测应用之外的其他应用的交互逻辑
patrol-native-tree({})
Patrol Test Architecture
Patrol测试架构
Structure
结构
- Use custom wrapper for all tests
testApp - Each test file should contain only ONE test
- Organize tests using ,
Modules, andSystempatternApiClients - Each module represents a feature from user perspective (eg. Auth, Home screen, Downloads, Player)
- System is a class for native interactions, specifically for actions that we need to perform by using $.platform that are not part of our app, eg. enabling airplane mode to test offline mode. Do not put methods here unless they don't fit any module of the app
- ApiClients - a class aggregating api clients for api communication. There will be separate clients for different apis we need to communicate with - eg. email server for testing, our test backend, or some third-party services
- 所有测试使用自定义的包装器
testApp - 每个测试文件应仅包含一个测试
- 使用、
Modules和System模式组织测试ApiClients - 每个模块代表用户视角下的一个功能(例如:Auth、首页、下载、播放器)
- System是用于原生交互的类,专门处理需要通过$.platform执行的、不属于本应用的操作,例如:开启飞行模式以测试离线模式。只有当操作不适合放入应用的任何模块时,才将方法放在此处
- ApiClients——聚合API客户端用于API通信的类。针对需要通信的不同API会有单独的客户端——例如:用于测试的邮件服务器、测试后端或某些第三方服务
Method organization in modules
模块中的方法组织
- Do not write comments in all methods, use descriptive names instead
- Split long methods in modules when the method is hard to read and navigate due to its length and it represents multiple logical steps that can be descriptively named
- If there is a series of steps which is reused in many tests as a whole (e.g., long onboarding flow) create a wrapper method in the module which will call private methods for each step
- 不要为所有方法添加注释,改用描述性命名替代
- 当模块中的方法因过长导致难以阅读和导航,且包含多个可单独命名的逻辑步骤时,拆分该长方法
- 如果有一系列步骤作为整体在多个测试中复用(例如:冗长的引导流程),在模块中创建一个包装方法,调用每个步骤对应的私有方法
Patrol Tests Rules
Patrol测试规则
Patrol API
Patrol API
- Any file that directly uses Patrol APIs (,
$(),.scrollTo(),.tap(),.enterText(), etc.) should.waitUntilVisible()import 'package:patrol/patrol.dart'; - ALWAYS inspect Patrol API before implementing test actions:
- Search codebase for existing Patrol API usage patterns
- Check $.platform APIs for the specific action
- If method not found in codebase, check: https://patrol.leancode.co/
- Only implement after confirming the correct API method
- ALWAYS inspect methods before implementing test actions
$.platform - Don't use flutter_test package. Use only patrol api
- Only run tests with Patrol MCP server for single test and for all tests. Never use
patrol testcommandflutter test - Do not write patrolSetUp and patrolTearDown methods on your own
- 任何直接使用Patrol API(、
$()、.scrollTo()、.tap()、.enterText()等)的文件都应.waitUntilVisible()import 'package:patrol/patrol.dart'; - 实现测试操作前,务必先查看Patrol API:
- 在代码库中搜索现有Patrol API的使用模式
- 查看$.platform中针对该操作的API
- 如果在代码库中未找到对应方法,查看:https://patrol.leancode.co/
- 确认正确的API方法后再实现
- 实现测试操作前,务必先查看方法
$.platform - 不要使用flutter_test包,仅使用Patrol API
- 仅通过Patrol MCP服务器运行单个测试,使用运行所有测试。绝不要使用
patrol test命令flutter test - 不要自行编写patrolSetUp和patrolTearDown方法
Action Rules
操作规则
- Don't use ,
$.pumporwaitUntilVisibleand other wait methods after or before tap, scrollTo and enterText. Patrol handles it automatically. Do this only at the end of the testwaitUntilExists - To find widgets, use only keys
- Don't write try catch blocks unless absolutely necessary
- After writing test check if it works by running it with MCP server, fix it if it fails
- If test fails because element was not found, check if it need to be scrolled to in the app code and adjust the test if needed
- 在tap、scrollTo和enterText操作之后或之前,不要使用、
$.pump或waitUntilVisible等等待方法。Patrol会自动处理这些逻辑。仅在测试末尾使用这些方法waitUntilExists - 仅使用键来查找组件
- 除非绝对必要,否则不要编写try catch块
- 编写完测试后,通过MCP服务器运行测试以检查是否可用,若失败则修复
- 如果测试因未找到元素而失败,检查应用代码中是否需要滚动到该元素,并按需调整测试
Assertion Rules
断言规则
- Don't write assertions after actions, but do write them at the end of the test
- Prefer using as assertion at the end of the test
waitUntilVisible - Use for assertions only when
expect()is not enoughwaitUntilVisible
- 不要在操作后编写断言,但要在测试末尾编写断言
- 优先使用作为测试末尾的断言
waitUntilVisible - 仅当无法满足需求时,才使用
waitUntilVisible进行断言expect()
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 over
$.platform.mobile.grantPermissionWhenInUse$.platform.mobile.tap
- 务必处理流程中出现的原生对话框:
- 在触发对话框的操作后立即处理对话框
- 对于原生权限,优先使用而非
$.platform.mobile.grantPermissionWhenInUse$.platform.mobile.tap
Test Keys
测试键
-
Assign a key ONLY to widgets involved in testing
-
ONLY add theparameter to existing widgets
key -
Create new keys.dart files if needed
-
Always have a maximum of one keys.dart file per feature directory, but the file can contain multiple classes
-
Add imports for keys.dart files
-
Update main keys.dart aggregator
-
NEVER change widget signatures
-
NEVER refactor existing code structure
-
NEVER hardcode keys in the app, you must use keys from the keys.dart file
-
NEVER create new widgets in the app
-
NEVER create a key that is not assigned to a widget
-
Always make sure that each key value is unique
-
ALWAYS create keys.dart to store keys for the feature/widget they belong to, NOT in the main lib directory
-
ALWAYS sort keys alphabetically
-
The mainclass in
Keysshould only import and aggregate keys from feature-specific keys.dart fileslib/keys.dart -
ALWAYS assign keys using this exact pattern:(unless you are using parameterized keys)
key: keys.feature.widgetName -
Add keys as first parameter to the widget constructor
-
Group related keys in a class named after the screen (e.g.). Use a private subclass of
HomeKeysto prefix all key values with the page or widget nameValueKey<String> -
For common widgets store them in WidgetKeys class. File containing this class should be placed in the directory that the common widget is defined
-
For widgets located in separate package (e.g. widgetbook) follow this pattern: indirectory create
widgetbookfile withkeys.dartclass. Those keys are assigned withWidgetKeys. Then, in keys.dart lib/ add it to the list of pages withwidgetKeys.tile(ds being the imported package, e.g.final widgetKeys = ds.widgetKeys;)import 'package:common_ui/widgets/keys.dart' as ds; -
If widget is not unique (for example generated from a list) use a parameterized key
- 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 and then use fixed values for it 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 the those enum/DTO values as the parameter
Use individual keys when:- Widgets are hardcoded and known at compile time
- Widgets have distinct, meaningful names
Use parameterized keys when:- Widgets are generated from dynamic data
- Widgets are generated from a DTO or enums
- Number of widgets is variable or large
- Widgets are generated in loops or from lists
Steps to assign a key to a widget:
- Identify widget that is needed for testing
- Create key assignment:
key: keys.feature.widgetName - Define key in keys.dart file
- Verify key is assigned to widget
File Structure Examples:
Feature-specific keys:
lib/features/home/keys.dart
lib/features/profile/keys.dart
lib/features/auth/keys.dart
lib/common/widgets/keys.dartMain keys aggregator:
lib/keys.dart (imports and aggregates all feature keys)Examples
Feature-specific keys.dart:
dart
// 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');
}Main keys aggregator:
dart
// 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();
}Grouping keys:
/lib/features/product/keys.dart
dart
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');
}Common widgets:
/lib/common/widgets/keys.dart
dart
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');
}
External packages:
widgetbook/keys.dart:
dart
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');
}
Importing keys from external packages to main lib/keys.dart:
dart
import 'package:common_ui/widgets/keys.dart' as ds;
final keys = Keys();
class Keys {
final widgetKeys = ds.widgetKeys;
}Example of assigning a parameterized key to a widget:
dart
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),
);
}
}keys.dart:
dart
_PickSizeKey sizeButton(SizeDTO size) => _PickSizeKey('sizeButton_${size.name}');-
仅为参与测试的组件分配键
-
仅向现有组件添加参数
key -
必要时创建新的keys.dart文件
-
每个功能目录最多只能有一个keys.dart文件,但该文件可包含多个类
-
添加keys.dart文件的导入
-
更新主keys.dart聚合器
-
绝不要修改组件签名
-
绝不要重构现有代码结构
-
绝不要在应用中硬编码键,必须使用keys.dart文件中的键
-
绝不要在应用中创建新组件
-
绝不要创建未分配给组件的键
-
务必确保每个键值都是唯一的
-
务必创建keys.dart来存储所属功能/组件的键,不要放在主lib目录下
-
务必按字母顺序对键进行排序
-
中的主
lib/keys.dart类应仅导入并聚合来自各功能专属keys.dart文件的键Keys -
务必使用以下精确模式分配键:(除非使用参数化键)
key: keys.feature.widgetName -
将键作为组件构造函数的第一个参数
-
按屏幕名称将相关键分组到类中(例如:)。使用
HomeKeys的私有子类,为所有键值添加页面或组件名称前缀ValueKey<String> -
通用组件的键存储在WidgetKeys类中。包含该类的文件应放在通用组件定义的目录中
-
对于位于单独包中的组件(例如:widgetbook),遵循以下模式:在目录中创建
widgetbook文件,包含keys.dart类。这些键通过WidgetKeys分配。然后在lib/keys.dart中,通过widgetKeys.tile将其添加到页面列表中(ds为导入的包,例如:final widgetKeys = ds.widgetKeys;)import 'package:common_ui/widgets/keys.dart' as ds; -
如果组件不唯一(例如:从列表生成),使用参数化键
- 务必优先使用已存在的枚举或DTO作为参数
- 使用组件的现有属性作为参数化键
- 绝不要在应用中分配参数化键后,在keys文件中使用固定值(反之亦然)
- 绝不要创建辅助方法,改用参数化键
- 当组件从现有枚举或DTO生成时,务必使用带有这些枚举/DTO值作为参数的参数化键
在以下情况使用独立键:
- 组件是硬编码的,编译时即可确定
- 组件具有独特且有意义的名称
在以下情况使用参数化键:
- 组件从动态数据生成
- 组件从DTO或枚举生成
- 组件数量可变或较多
- 组件通过循环或列表生成
为组件分配键的步骤:
- 确定测试所需的组件
- 创建键分配:
key: keys.feature.widgetName - 在keys.dart文件中定义键
- 验证键已分配给组件
文件结构示例:
功能专属键:
lib/features/home/keys.dart
lib/features/profile/keys.dart
lib/features/auth/keys.dart
lib/common/widgets/keys.dart主键聚合器:
lib/keys.dart (导入并聚合所有功能键)示例
功能专属keys.dart:
dart
// 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');
}主键聚合器:
dart
// 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();
}键分组:
/lib/features/product/keys.dart
dart
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');
}通用组件:
/lib/common/widgets/keys.dart
dart
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');
}
外部包:
widgetbook/keys.dart:
dart
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');
}
将外部包的键导入到主lib/keys.dart:
dart
import 'package:common_ui/widgets/keys.dart' as ds;
final keys = Keys();
class Keys {
final widgetKeys = ds.widgetKeys;
}为组件分配参数化键的示例:
dart
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),
);
}
}keys.dart:
dart
_PickSizeKey sizeButton(SizeDTO size) => _PickSizeKey('sizeButton_${size.name}');Code Examples
代码示例
Module Structure
模块结构
Feature module example
patrol_test/modules/home.dart
dart
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();
}
}Modules aggregator
patrol_test/modules/modules.dart
dart
final class Modules {
Modules(this._$);
final PatrolIntegrationTester _$;
late final home = Home(_$);
late final auth = Auth(_$);
}功能模块示例
patrol_test/modules/home.dart
dart
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();
}
}模块聚合器
patrol_test/modules/modules.dart
dart
final class Modules {
Modules(this._$);
final PatrolIntegrationTester _$;
late final home = Home(_$);
late final auth = Auth(_$);
}System Class
System类
patrol_test/modules/system.dart
dart
final class System extends PlatformAutomator {
System({required super.config});
Future<void> checkIfNativePlayerIsVisible() async {
// Implementation
}
}patrol_test/modules/system.dart
dart
final class System extends PlatformAutomator {
System({required super.config});
Future<void> checkIfNativePlayerIsVisible() async {
// Implementation
}
}ApiClients Class
ApiClients类
patrol_test/modules/api_clients.dart
dart
final class ApiClients {
final backend = BackendClient();
final mailpitClient = MailpitClient();
}patrol_test/modules/api_clients.dart
dart
final class ApiClients {
final backend = BackendClient();
final mailpitClient = MailpitClient();
}Complete Test Example
完整测试示例
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: '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();
});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: '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();
});