Maestro is an open-source framework that makes UI and end-to-end testing for Android, iOS, and web apps simple and fast.
Write your first test in under five minutes using YAML flows and run them on any emulator, simulator, or browser.
---
## Table of Contents
- [Why Maestro?](#why-maestro)
- [Getting Started](#getting-started)
- [Resources & Community](#resources--community)
- [Contributing](#contributing)
- [Maestro Studio – Test IDE](#maestro-studio--test-ide)
- [Maestro Cloud – Parallel Execution & Scalability](#maestro-cloud--parallel-execution--scalability)
---
## Why Maestro?
Maestro is built on learnings from its predecessors (Appium, Espresso, UIAutomator, XCTest, Selenium, Playwright) and allows you to easily define and test your Flows.
By combining a human-readable YAML syntax with an interpreted execution engine, it lets you write, run, and scale cross-platform end-to-end tests for mobile and web with ease.
- **Cross-platform coverage** – test Android, iOS, and web apps (React Native, Flutter, hybrid) on emulators, simulators, or real devices.
- **Human-readable YAML flows** – express interactions as commands like `launchApp`, `tapOn`, and `assertVisible`.
- **Resilience & smart waiting** – built-in flakiness tolerance and automatic waiting handle dynamic UIs without manual `sleep()` calls.
- **Fast iteration & simple install** – flows are interpreted (no compilation) and installation is a single script.
**Simple Example:**
```
# flow_contacts_android.yaml
appId: com.android.contacts
---
- launchApp
- tapOn: "Create new contact"
- tapOn: "First Name"
- inputText: "John"
- tapOn: "Last Name"
- inputText: "Snow"
- tapOn: "Save"
```
---
## Getting Started
Maestro requires Java 17 or higher to be installed on your system. You can verify your Java version by running:
```
java -version
```
Installing the CLI:
Run the following command to install Maestro on macOS, Linux or Windows (WSL):
```
curl -fsSL "https://get.maestro.mobile.dev" | bash
```
The links below will guide you through the next steps.
- [Installing Maestro](https://docs.maestro.dev/getting-started/installing-maestro) (includes regular Windows installation)
- [Build and install your app](https://docs.maestro.dev/getting-started/build-and-install-your-app)
- [Run a sample flow](https://docs.maestro.dev/getting-started/run-a-sample-flow)
- [Writing your first flow](https://docs.maestro.dev/getting-started/writing-your-first-flow)
---
## Resources & Community
- 💬 [Join the Slack Community](https://maestrodev.typeform.com/to/FelIEe8A)
- 📘 [Documentation](https://docs.maestro.dev)
- 📰 [Blog](https://maestro.dev/blog?utm_source=github-readme)
- 🐦 [Follow us on X](https://twitter.com/maestro__dev)
---
## Contributing
Maestro is open-source under the Apache 2.0 license — contributions are welcome!
- Check [good first issues](https://github.com/mobile-dev-inc/maestro/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
- Read the [Contribution Guide](https://github.com/mobile-dev-inc/Maestro/blob/main/CONTRIBUTING.md)
- Fork, create a branch, and open a Pull Request.
If you find Maestro useful, ⭐ star the repository to support the project.
---
## Maestro Studio – Test IDE
**Maestro Studio Desktop** is a lightweight IDE that lets you design and execute tests visually — no terminal needed.
It is also free, even though Studio is not an open-source project. So you won't find the Maestro Studio code here.
- **Simple setup** – just download the native app for macOS, Windows, or Linux.
- **Visual flow builder & inspector** – record interactions, inspect elements, and build flows visually.
- **AI assistance** – use MaestroGPT to generate commands and answer questions while authoring tests.
[Download Maestro Studio](https://maestro.dev/?utm_source=github-readme#maestro-studio)
---
## Maestro Cloud – Parallel Execution & Scalability
When your test suite grows, run hundreds of tests in parallel on dedicated infrastructure, cutting execution times by up to 90%. Includes built-in notifications, deterministic environments, and complete debugging tools.
Pricing for Maestro Cloud is completely transparent and can be found on the [pricing page](https://maestro.dev/pricing?utm_source=github-readme).
👉 [Start your free 7-day trial](https://maestro.dev/cloud?utm_source=github-readme)
```
Built with ❤️ by Maestro.dev
```
================================================
FILE: RELEASING.md
================================================
# Production Releases
## Prepare
1. Define the next semantic version
Semantic versioning: a.b.c
- a: major breaking changes
- b: new functionality, new features
- c: any other small changes
2. Checkout the main branch and make sure it is up-to-date: `git checkout main && git pull`
3. Create a new branch
4. Update the CHANGELOG.md file with changes of this release, you should add a new section with your version number and the relevant updates, like the ones that exist on the previous versions
5. Change the version in `gradle.properties`
6. Change the version in `maestro-cli/gradle.properties`
7. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version)
8. Submit a PR with the changes against the main branch
9. Merge the PR
## Tag
1. `git tag -a vX.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version)
2. `git push --tags`
3. Wait until all Publish actions have completed https://github.com/mobile-dev-inc/maestro/actions
## Publish Maven Central
1. Trigger the [Publish Release action](https://github.com/mobile-dev-inc/maestro/actions/workflows/publish-release.yml)
- ATTENTION: Wait for it to finish
3. Go to [OSS Sonatype](https://s01.oss.sonatype.org/) and login with user/password
4. Go to Staging Repositories, select the repository uploaded from the trigger above.
5. Click "Close" and then "Release". Each of these operations take a couple minutes to complete
____________________________________________________________________________________________________________________________________________________
**CAUTION:** You should go back to the [notion document](https://www.notion.so/Maestro-Release-Run-Book-78159c6f80de4492a6e9e05bb490cf60?pvs=4) to see how to update the **Robin** and **Maestro Cloud** versions before updating the **CLI**
____________________________________________________________________________________________________________________________________________________
## Publish CLI
1. Trigger the [Publish CLI Github action](https://github.com/mobile-dev-inc/Maestro/actions/workflows/publish-cli.yaml)
2. Test installing the cli by running `curl -Ls "https://get.maestro.mobile.dev" | bash`
3. Check the version number `maestro --version`
================================================
FILE: build.gradle.kts
================================================
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.protobuf) apply false
alias(libs.plugins.mavenPublish)
alias(libs.plugins.detekt)
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
tasks.named("compileKotlin", KotlinCompilationTask::class.java) {
compilerOptions {
freeCompilerArgs.addAll("-Xjdk-release=17")
}
}
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
detekt {
buildUponDefaultConfig = true
allRules = false
autoCorrect = true
config = files("${rootDir}/detekt.yml")
}
================================================
FILE: detekt.yml
================================================
build:
maxIssues: 0
excludeCorrectable: false
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
config:
validation: true
warningsAsErrors: false
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
excludes: ''
processors:
active: true
exclude:
- 'DetektProgressListener'
# - 'KtFileCountProcessor'
# - 'PackageCountProcessor'
# - 'ClassCountProcessor'
# - 'FunctionCountProcessor'
# - 'PropertyCountProcessor'
# - 'ProjectComplexityProcessor'
# - 'ProjectCognitiveComplexityProcessor'
# - 'ProjectLLOCProcessor'
# - 'ProjectCLOCProcessor'
# - 'ProjectLOCProcessor'
# - 'ProjectSLOCProcessor'
# - 'LicenseHeaderLoaderExtension'
console-reports:
active: true
exclude:
- 'ProjectStatisticsReport'
- 'ComplexityReport'
- 'NotificationReport'
# - 'FindingsReport'
- 'FileBasedFindingsReport'
- 'LiteFindingsReport'
output-reports:
active: true
exclude:
# - 'TxtOutputReport'
# - 'XmlOutputReport'
# - 'HtmlOutputReport'
comments:
active: true
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: 'license.template'
licenseTemplateIsRegex: false
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
DeprecatedBlockTag:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
OutdatedDocumentation:
active: false
matchTypeParameters: true
matchDeclarationsOrder: true
UndocumentedPublicClass:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
searchInNestedClass: true
searchInInnerClass: true
searchInInnerObject: true
searchInInnerInterface: true
UndocumentedPublicFunction:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
UndocumentedPublicProperty:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
complexity:
active: true
ComplexCondition:
active: true
threshold: 4
ComplexInterface:
active: false
threshold: 10
includeStaticDeclarations: false
includePrivateDeclarations: false
ComplexMethod:
active: true
threshold: 15
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
nestingFunctions:
- 'also'
- 'apply'
- 'forEach'
- 'isNotNull'
- 'ifNull'
- 'let'
- 'run'
- 'use'
- 'with'
LabeledExpression:
active: false
ignoredLabels: [ ]
LargeClass:
active: true
threshold: 600
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
LongMethod:
active: true
threshold: 120
LongParameterList:
active: false
functionThreshold: 6
constructorThreshold: 7
ignoreDefaultParameters: false
ignoreDataClasses: true
ignoreAnnotatedParameter: [ ]
MethodOverloading:
active: false
threshold: 6
NamedArguments:
active: false
threshold: 3
NestedBlockDepth:
active: true
threshold: 4
ReplaceSafeCallChainWithRun:
active: false
StringLiteralDuplication:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
thresholdInFiles: 11
thresholdInClasses: 11
thresholdInInterfaces: 11
thresholdInObjects: 11
thresholdInEnums: 11
ignoreDeprecated: false
ignorePrivate: false
ignoreOverridden: false
coroutines:
active: true
GlobalCoroutineUsage:
active: false
InjectDispatcher:
active: false
dispatcherNames:
- 'IO'
- 'Default'
- 'Unconfined'
RedundantSuspendModifier:
active: false
SleepInsteadOfDelay:
active: false
SuspendFunWithFlowReturnType:
active: false
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: '_|(ignore|expected).*'
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: true
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: true
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyTryBlock:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames:
- 'equals'
- 'finalize'
- 'hashCode'
- 'toString'
InstanceOfCheckForException:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
NotImplementedDeclaration:
active: false
ObjectExtendsThrowable:
active: false
PrintStackTrace:
active: true
RethrowCaughtException:
active: true
ReturnFromFinally:
active: true
ignoreLabeled: false
SwallowedException:
active: true
ignoredExceptionTypes:
- 'InterruptedException'
- 'MalformedURLException'
- 'NumberFormatException'
- 'ParseException'
allowedExceptionNameRegex: '_|(ignore|expected).*'
ThrowingExceptionFromFinally:
active: true
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
exceptions:
- 'ArrayIndexOutOfBoundsException'
- 'Exception'
- 'IllegalArgumentException'
- 'IllegalMonitorStateException'
- 'IllegalStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
exceptionNames:
- 'ArrayIndexOutOfBoundsException'
- 'Error'
- 'Exception'
- 'IllegalMonitorStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
allowedExceptionNameRegex: '_|(ignore|expected).*'
TooGenericExceptionThrown:
active: true
exceptionNames:
- 'Error'
- 'Exception'
- 'RuntimeException'
- 'Throwable'
formatting:
active: true
android: false
autoCorrect: true
AnnotationOnSeparateLine:
active: false
autoCorrect: true
AnnotationSpacing:
active: false
autoCorrect: true
ArgumentListWrapping:
active: false
autoCorrect: true
indentSize: 4
maxLineLength: 160
ChainWrapping:
active: true
autoCorrect: true
CommentSpacing:
active: true
autoCorrect: true
EnumEntryNameCase:
active: false
autoCorrect: true
Filename:
active: true
FinalNewline:
active: true
autoCorrect: true
insertFinalNewLine: true
ImportOrdering:
active: true
autoCorrect: true
layout: '*,java.**,javax.**,kotlin.**,^'
Indentation:
active: true
autoCorrect: true
indentSize: 4
continuationIndentSize: 4
MaximumLineLength:
active: true
maxLineLength: 160
ignoreBackTickedIdentifier: false
ModifierOrdering:
active: true
autoCorrect: true
MultiLineIfElse:
active: false
autoCorrect: true
NoBlankLineBeforeRbrace:
active: true
autoCorrect: true
NoConsecutiveBlankLines:
active: true
autoCorrect: true
NoEmptyClassBody:
active: true
autoCorrect: true
NoEmptyFirstLineInMethodBlock:
active: false
autoCorrect: true
NoLineBreakAfterElse:
active: true
autoCorrect: true
NoLineBreakBeforeAssignment:
active: true
autoCorrect: true
NoMultipleSpaces:
active: true
autoCorrect: true
NoSemicolons:
active: true
autoCorrect: true
NoTrailingSpaces:
active: true
autoCorrect: true
NoUnitReturn:
active: true
autoCorrect: true
NoUnusedImports:
active: true
autoCorrect: true
NoWildcardImports:
active: true
PackageName:
active: false
autoCorrect: true
ParameterListWrapping:
active: true
autoCorrect: true
indentSize: 4
maxLineLength: 160
SpacingAroundAngleBrackets:
active: false
autoCorrect: true
SpacingAroundColon:
active: true
autoCorrect: true
SpacingAroundComma:
active: true
autoCorrect: true
SpacingAroundCurly:
active: true
autoCorrect: true
SpacingAroundDot:
active: true
autoCorrect: true
SpacingAroundDoubleColon:
active: false
autoCorrect: true
SpacingAroundKeyword:
active: true
autoCorrect: true
SpacingAroundOperators:
active: true
autoCorrect: true
SpacingAroundParens:
active: true
autoCorrect: true
SpacingAroundRangeOperator:
active: true
autoCorrect: true
SpacingAroundUnaryOperator:
active: false
autoCorrect: true
SpacingBetweenDeclarationsWithAnnotations:
active: false
autoCorrect: true
SpacingBetweenDeclarationsWithComments:
active: false
autoCorrect: true
StringTemplate:
active: true
autoCorrect: true
naming:
active: true
BooleanPropertyNaming:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
allowedPattern: '^(is|has|are)'
ClassNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
classPattern: '[A-Z][a-zA-Z0-9]*'
ConstructorParameterNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
EnumNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
forbiddenName: [ ]
FunctionMaxLength:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
minimumFunctionNameLength: 3
FunctionNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
excludeClassPattern: '$^'
ignoreOverridden: true
FunctionParameterNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
InvalidPackageDeclaration:
active: false
rootPackage: ''
LambdaParameterNaming:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
parameterPattern: '[a-z][A-Za-z0-9]*|_'
MatchingDeclarationName:
active: true
mustBeFirst: true
MemberNameEqualsClassName:
active: true
ignoreOverridden: true
NoNameShadowing:
active: false
NonBooleanPropertyPrefixedWithIs:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
ObjectPropertyNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
maximumVariableNameLength: 64
VariableMinLength:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
minimumVariableNameLength: 1
VariableNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
performance:
active: true
ArrayPrimitive:
active: true
ForEachOnRange:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
SpreadOperator:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
AvoidReferentialEquality:
active: false
forbiddenTypePatterns:
- 'kotlin.String'
CastToNullableType:
active: false
Deprecation:
active: false
DontDowncastCollectionTypes:
active: false
DoubleMutabilityForCollection:
active: false
DuplicateCaseInWhenExpression:
active: true
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExitOutsideMain:
active: false
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: false
IgnoredReturnValue:
active: false
restrictToAnnotatedMethods: true
returnValueAnnotations:
- '*.CheckResult'
- '*.CheckReturnValue'
ignoreReturnValueAnnotations:
- '*.CanIgnoreReturnValue'
ImplicitDefaultLocale:
active: true
ImplicitUnitReturnType:
active: false
allowExplicitReturnType: true
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
active: true
IteratorNotThrowingNoSuchElementException:
active: true
LateinitUsage:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
ignoreOnClassesPattern: ''
MapGetWithNotNullAssertionOperator:
active: false
MissingPackageDeclaration:
active: false
excludes: [ '**/*.kts' ]
MissingWhenCase:
active: true
allowElseExpression: true
NullableToStringCall:
active: false
RedundantElseInWhen:
active: true
UnconditionalJumpStatementInLoop:
active: false
UnnecessaryNotNullOperator:
active: true
UnnecessarySafeCall:
active: true
UnreachableCatchBlock:
active: false
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
UnsafeCast:
active: true
UnusedUnaryOperator:
active: false
UselessPostfixExpression:
active: false
WrongEqualsTypeParameter:
active: true
style:
active: true
ClassOrdering:
active: false
CollapsibleIfStatements:
active: false
DataClassContainsFunctions:
active: false
conversionFunctionPrefix: 'to'
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: false
maxDestructuringEntries: 3
EqualsNullCall:
active: true
EqualsOnSignatureLine:
active: false
ExplicitCollectionElementAccessMethod:
active: false
ExplicitItLambdaParameter:
active: false
ExpressionBodySyntax:
active: false
includeLineWrapping: false
ForbiddenComment:
active: true
values:
- 'FIXME:'
- 'STOPSHIP:'
- 'TODO:'
allowedPatterns: ''
customMessage: ''
ForbiddenImport:
active: false
imports: [ ]
forbiddenPatterns: ''
ForbiddenMethodCall:
active: false
methods:
- 'kotlin.io.print'
- 'kotlin.io.println'
ForbiddenPublicDataClass:
active: true
excludes: [ '**' ]
ignorePackages:
- '*.internal'
- '*.internal.*'
ForbiddenVoid:
active: false
ignoreOverridden: false
ignoreUsageInGenerics: false
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
ignoreActualFunction: true
excludedFunctions: ''
LibraryCodeMustSpecifyReturnType:
active: true
excludes: [ '**' ]
LibraryEntitiesShouldNotBePublic:
active: true
excludes: [ '**' ]
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
MagicNumber:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
ignoreNumbers:
- '-1'
- '0'
- '1'
- '2'
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreLocalVariableDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
ignoreRanges: false
ignoreExtensionFunctions: true
MandatoryBracesIfStatements:
active: false
MandatoryBracesLoops:
active: false
MaxLineLength:
active: true
maxLineLength: 160
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
MayBeConst:
active: true
ModifierOrder:
active: true
MultilineLambdaItParameter:
active: false
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
NoTabs:
active: false
ObjectLiteralToLambda:
active: false
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: false
OptionalWhenBraces:
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:
active: true
RedundantExplicitType:
active: false
RedundantHigherOrderMapUsage:
active: false
RedundantVisibilityModifierRule:
active: false
ReturnCount:
active: false
max: 2
excludedFunctions: 'equals'
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: true
SpacingBetweenPackageAndImports:
active: false
ThrowsCount:
active: false
max: 2
excludeGuardClauses: false
TrailingWhitespace:
active: false
UnderscoresInNumericLiterals:
active: false
acceptableLength: 4
UnnecessaryAbstractClass:
active: false
UnnecessaryAnnotationUseSiteTarget:
active: false
UnnecessaryApply:
active: true
UnnecessaryFilter:
active: false
UnnecessaryInheritance:
active: true
UnnecessaryLet:
active: false
UnnecessaryParentheses:
active: false
UntilInsteadOfRangeTo:
active: false
UnusedImports:
active: false
UnusedPrivateClass:
active: true
UnusedPrivateMember:
active: true
allowedNames: '(_|ignored|expected|serialVersionUID)'
UseAnyOrNoneInsteadOfFind:
active: false
UseArrayLiteralsInAnnotations:
active: false
UseCheckNotNull:
active: false
UseCheckOrError:
active: false
UseDataClass:
active: false
allowVars: false
UseEmptyCounterpart:
active: false
UseIfEmptyOrIfBlank:
active: false
UseIfInsteadOfWhen:
active: false
UseIsNullOrEmpty:
active: false
UseOrEmpty:
active: false
UseRequire:
active: false
UseRequireNotNull:
active: false
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: true
WildcardImport:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
excludeImports:
- 'java.util.*'
================================================
FILE: e2e/.gitignore
================================================
apps/
samples/
samples.zip
================================================
FILE: e2e/README.md
================================================
# e2e
This directory contains glue code for testing Maestro itself.
## Testing
Typical workflow is:
1. Start Android emulator and iOS simulator
2. `download_apps`
3. `install_apps`
4. `run_tests`
We try to keep shell code in separate files, so we don't get too tightly coupled
to GitHub Actions.
### Expected failures
Let's say a critical bug is introduced that causes Maestro to always mark all
tests as passed. If our e2e test suite only was only checking if all tests pass
(i.e. `maestro test` exit code is 0), then wouldn't catch such a bug.
To prevent this, all flows in this directory MUST have a `passing` or `failing`
label, so the correct outcome can be asserted.
## Samples
This directory also contains samples that are downloaded by the `maestro download-samples` command,
and some glue code to facilitate updating those samples.
`maestro download-samples` provides a set of flows and apps so that users can
quickly try out Maestro, without having to write any flows for their own app.
`download-samples` downloads these files and apps from our publicly-available
Google Cloud Storage bucket (hosted on `storage.googleapis.com`).
### Intro
The samples are automatically updated by the GitHub Action on every new commit
to the `main` branch.
There zip archive that is downloaded by `download-samples` consists of 2 things:
- the Maestro workspace with flows (located in the `workspaces/wikipedia` directory)
- the app binary files that are used in the flows (located in the `apps` directory)
App binary files are heavy, so we don't store them in the repository. Instead, they are hosted
on publicly available directory in Google Cloud Storage:
### Update the samples
Run the script:
```console
./update_samples
```
================================================
FILE: e2e/download_apps
================================================
#!/usr/bin/env sh
set -eu
# Download apps from URLs listed in manifest.txt.
#
# We assume that if the downloaded file is a zip file, it's an iOS app and must
# be unzipped.
[ "$(basename "$PWD")" = "e2e" ] || { echo "must be run from e2e directory" && exit 1; }
command -v curl >/dev/null 2>&1 || { echo "curl is required" && exit 1; }
platform="${1:-}" # android or ios or an empty string (no filter)
platform="$(echo "$platform" | tr '[:upper:]' '[:lower:]')" # Normalize to lowercase
mkdir -p ./apps
while read -r url; do
case "$platform" in
android)
echo "$url" | grep -qi '\.apk$' || continue # Skip if not an APK
;;
ios)
echo "$url" | grep -qi '\.zip$' || continue # Skip if not a ZIP file
;;
*)
# No filter
;;
esac
echo "download $url"
app_file="$(curl -fsSL --output-dir ./apps --write-out "%{filename_effective}" -OJ "$url")"
extension="${app_file##*.}"
if [ "$extension" = "zip" ]; then
unzip -qq -o -d ./apps "$app_file" -x "__MACOSX/*"
fi
done