diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..b410147ce --- /dev/null +++ b/404.html @@ -0,0 +1,1299 @@ + + + + + + + + + + + + + + + + + + + Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Home/Breaking-changes/index.html b/Home/Breaking-changes/index.html new file mode 100644 index 000000000..72168e9ca --- /dev/null +++ b/Home/Breaking-changes/index.html @@ -0,0 +1,1453 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Breaking changes - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Breaking changes

+ +

1.2.0

+
    +
  • We've totally reworked AdbServer and Kaspresso 1.2.0 works only with new artifacts/adbserver-desktop.jar
    + The old version artifacts/desktop_1_1_0.jar is also available for use with older versions of Kaspresso.
  • +
  • If you use device.logcat in your tests, you should call device.logcat.disableChatty in the before section of your test. + In previous version of Kaspresso, device.logcat.disableChatty was called automatically during initialization. This resulted in the need to always run AdbServer before tests.
  • +
+

1.2.1

+
    +
  • Kaspresso migrated to a new version of Kakao which has io.github.kakaocup.kakao package name. Replace all imports using command + find . -type f \( -name "*.kt" -o -name "*.java" \) -print0 | xargs -0 sed -i '' -e 's/com.agoda/io.github.kakaocup/g' or using global replacement tool in IDE.
  • +
+

1.5.0

+
    +
  • In order to support the system storage restrictions artifacts are saved under /sdcard/Documents folder. + Video recording in the allure tests requires using new kaspresso builder: Kaspresso.Builder.withForcedAllureSupport() and replacing the test runner (io.qameta.allure.android.runners.AllureAndroidJUnitRunner) with com.kaspersky.kaspresso.runner.KaspressoRunner + Deprecated TestFailRule. Fixed fail test screenshotting + Fixed an automatic system dialogs closing. See this diff.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Home/Contribution_guide/index.html b/Home/Contribution_guide/index.html new file mode 100644 index 000000000..497bbd14a --- /dev/null +++ b/Home/Contribution_guide/index.html @@ -0,0 +1,1455 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Contribution guide - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Contribution guide

+

Сode contribution workflow

+
    +
  1. Find an open issue or create a new issue on issue tracker for the feature you want to contribute.
  2. +
  3. Fork the project on GitHub. You need to create a feature-branch for your work on your fork, as this way you be able to submit a pull request.
  4. +
  5. Make any necessary changes to the source code.
  6. +
  7. Add tests that verify that your contribution works as expected and modify existing tests if required.
  8. +
  9. Run all unit and UI tests and make sure all of them pass.
  10. +
  11. Run code coverage to check if the lines of code you added are covered by unit tests.
  12. +
  13. Once your feature is complete, prepare the commit with appropriate message and the issue number.
  14. +
  15. Create a pull request and wait for the users to review. When you submit a pull request, please, agree to the terms of CLA.
  16. +
  17. Once everything is done, your pull request gets merged. Your feature will be available with the next release and your name will be added to AUTHORS.
  18. +
+

Branch naming

+

issue-***/detailed_description. Example: issue-306/fix-padding-breaks-autoscroll-interceptor

+

Commits

+

The commit message should begin with: "Issue #***: ...". Example: "Issue #306: Fixed padding-breaks autoscroll interceptor".

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Home/Kaspresso users/index.html b/Home/Kaspresso users/index.html new file mode 100644 index 000000000..fdae10280 --- /dev/null +++ b/Home/Kaspresso users/index.html @@ -0,0 +1,1439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Our users - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Our Users

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ www.kaspersky.ru + + hh.ru + + aliexpress.ru + + www.sber.ru + + www.revolut.com +
+ www.delivery-club.ru + + www.vtb.ru + + www.tinkoff.ru + + www.x5.ru + + www.zen.yandex.ru +
+ www.psbank.ru + + www.letoile.ru + + rtkit.ru + + ooo.technology + + www.blinkist.com +
+ www.rabota.ru + + www.cian.ru + + squaregps.com + + nexign.com + + profi.ru +
+ alohabrowser.com + + vivid.money + + raiffeisen.ru + + cft.ru + + superjob.ru +
+ dmtech.team +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Home/Kaspresso-in-articles/index.html b/Home/Kaspresso-in-articles/index.html new file mode 100644 index 000000000..c87e1ca2d --- /dev/null +++ b/Home/Kaspresso-in-articles/index.html @@ -0,0 +1,1364 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Kaspresso in articles - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Kaspresso in articles

+

[EN] Eugene Matsyuk — Kaspresso: The autotest framework that you have been looking forward to. Part I
+[EN] Feyza Dayan - UI Test Roadmap with Kaspresso
+[EN] Kaspersky makes Android app testing tool publicly available for mobile developers
+[EN] Danil Perevalov - Leak detection into UI tests
+[EN] Christina Rozenkova - Kaspresso Tutorials. Part 1. Launching the First Test
+[EN] Evgenii Matsiuk (Eugene Matsyuk) - Autotests on Android. The entire picture
+[EN] Senchurin Nick - Kaspresso and ADB server

+

[RU] Евгений Мацюк — Kaspresso: фреймворк для автотестирования, который вы ждали
+[RU] Иван Федянин — Kaspresso tutorials. Часть 1. Запуск первого теста
+[RU] Евгений Мацюк — Что там по автотестам на Android в 2022?
+[RU] Konstantin Sidorov - Путь к автотестированию Android нативными инструментами: испробовали всё, что есть на рынке и сделали свои выводы
+[RU] Алексей Пак - Настраиваем CI/CD для тестовой инфраструктуры Android
+[RU] Егор Курников - На чем писать Android UI-тесты

+
+

Do you want your article to be included in this list? Everything is simple! Write an article, send it to us and we will add it to this list! +

+
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Home/Kaspresso-in-videos/index.html b/Home/Kaspresso-in-videos/index.html new file mode 100644 index 000000000..60eadf668 --- /dev/null +++ b/Home/Kaspresso-in-videos/index.html @@ -0,0 +1,1357 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Kaspresso in videos - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Issues/Storage_issue/index.html b/Issues/Storage_issue/index.html new file mode 100644 index 000000000..a76a520f1 --- /dev/null +++ b/Issues/Storage_issue/index.html @@ -0,0 +1,1384 @@ + + + + + + + + + + + + + + + + + + + + + + + Storage issues - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Storage issues

+ +
+

Info

+

The problem described below is relevant for versions of Kaspresso below 1.5.0. Starting with this version, Kaspresso fully supports the new format of working with system storage.

+
+

Kaspresso can use external storage to save various data about executed tests. The example of such data is screenshots, xml dumps, logs, video and anymore. +But, new Android OS provides absolutely new way to work with external storage - Scoped Storage. Currently, we are working on the support of Scoped Storage. +On versions of Kaspresso prior to 1.5.0, work with Scoped storage is supported only by requesting various permissions. +Here, it's a detailed instruction:

+
    +
  1. AndroidManifest.xml (in your debug build variant to keep production manifest without any changes) +
    # Please, add these permissions
    +<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    +<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    +<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
    +
    +<application
    +    # storage support for Android API 29         
    +    android:requestLegacyExternalStorage="true"
    +    ...
    +</application>             
    +
  2. +
  3. Your test class: +
    class SampleTest : TestCase(
    +    kaspressoBuilder = Kaspresso.Builder.simple( // simple/advanced - it doesn't matter
    +        customize = { 
    +            // storage support for Android API 30+
    +            if (isAndroidRuntime) {
    +                UiDevice
    +                    .getInstance(instrumentation)
    +                    .executeShellCommand("appops set --uid ${InstrumentationRegistry.getInstrumentation().targetContext.packageName} MANAGE_EXTERNAL_STORAGE allow")
    +            }
    +        }
    +    )
    +) {
    +
    +    // storage support for Android API 29-
    +    @get:Rule
    +    val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
    +        Manifest.permission.WRITE_EXTERNAL_STORAGE,
    +        Manifest.permission.READ_EXTERNAL_STORAGE
    +    )
    +
    +    //...
    +}    
    +
  4. +
+

This is a temporary solution. We recommend migrating to the latest version of Kaspresso (1.5.0 and above) to avoid these problems.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Issues/index.html b/Issues/index.html new file mode 100644 index 000000000..edf5f8084 --- /dev/null +++ b/Issues/index.html @@ -0,0 +1,1547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Found an issue? - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Found an issue?

+

Kaspresso has a great community that helps make it better by suggesting new ideas, reporting bugs with detailed descriptions and making pull requests.

+

Creating new issues

+

In our Issues tab you can create a new one. There are two most popular types of issues: bug and enhancement.

+

Template for bugs

+

If you found a bug you can create new issue. Enter a title and provide a description (bug details) in the input fields. We will be very grateful if you use this template:

+
Description:
+...
+Expected Behavior:
+...
+Actual Behavior:
+...
+Steps to Reproduce the Problem:
+...
+Specifications:
+...
+
+

For example: +

When using newer versions of the library, Gradle is unable to find and download the library sources (which allow you to read and debug the source code directly on the IDE).
+
+Expected Behavior
+Projects with the Kaspresso dependency should be able to download sources.
+
+Actual Behavior
+When trying do download sources, the following error appears:
+
+* What went wrong:
+Execution failed for task ':app:DownloadSources'.
+> Could not resolve all files for configuration ':app:downloadSources_10c6f7e9-408b-4f6a-8bd9-fe15e255981e'.
+   > Could not find com.kaspersky.android-components:kaspresso:1.4.1@aar.
+     Searched in the following locations:
+       - https://dl.google.com/dl/android/maven2/com/kaspersky/android-components/kaspresso/1.4.1@aar/kaspresso-1.4.1@aar.pom
+       - https://repo.maven.apache.org/maven2/com/kaspersky/android-components/kaspresso/1.4.1@aar/kaspresso-1.4.1@aar.pom
+     Required by:
+         project :app
+
+Steps to Reproduce the Problem
+Create an empty project;
+Add the dependency androidTestImplementation "com.kaspersky.android-components:kaspresso:1.4.1";
+Create a test using classes from Kaspresso;
+Try to access the source (on IntelliJ IDE, Ctrl+left click a Kaspresso class name/method call);
+You will only be able to see the decompiled bytecode.
+Specifications
+Library version: at least >= 1.4.1
+IDE used: Android Studio
+
+Observations
+I haven't tested on all versions, but sources were able to be downloaded at least up to version 1.2.1.
+

+

Template for enhancements

+

If you have an idea of a new enhancement you can create new issue. Enter a title and provide a description in the input fields. We will be very grateful if you use this template:

+
Description:
+...
+How the new enhancement will help?:
+...
+Existing analogs (with links):
+...
+
+

Pull requests are allways welcome

+

If you have not only an issue, but also a ready implementation, you can always submit the pull request on Github.

+

Thanks!

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Android_permissions/index.html b/Tutorial/Android_permissions/index.html new file mode 100644 index 000000000..fb092d24a --- /dev/null +++ b/Tutorial/Android_permissions/index.html @@ -0,0 +1,2297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 10. Working with Android permissions - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Test apps that require permissions

+

In this tutorial, we will learn how to work with permissions (Permissions).

+

Often, in order to work correctly, an application needs access to certain functions of the mobile device: to the camera, voice recording, making calls, sending SMS messages, etc. The application can access and use them only if the user gives permission to do so.

+

On older devices below the sixth version of Android (API level 23), such permissions were requested at the time the application was installed, and if the user installed it, it was considered that he agreed with all the permissions, and the application would be able to use all the necessary functions. This was unsafe, as it opened up the possibility for unscrupulous developers to gain access to the microphone, camera, calls and other important components without the user noticing and use it for their own purposes.

+

For this reason, on newer versions, the so-called "dangerous" permissions began to be requested not at the time of installation, but while the application was running. Now the user will clearly see a dialog with a proposal to allow or deny a request to use some functionality.

+

For example, run the tutorial application on one of the latest versions of Android (API 23 and above) and press the Make Call Activity button

+

Main Screen

+

You will see a screen on which there are two elements - an input field and a button. In the input field, you can specify some phone number and click on the Make Call button to make a call

+

Make call screen

+

Making calls is one of the features that requires permission from the user to work. Therefore, you will see a dialog asking you to allow the application to control calls, which has "Allow" and "Reject" buttons.

+

Request permissions

+

If we click “Allow”, then the call will begin to the subscriber at the number that you specified in the input field

+

Calling

+

The next time you open the application, the permission will no longer be requested, it is saved on the device. If you want to revoke permission, you can do so in the settings. To do this, go to the application section, find the one you need and go to the Permissions section

+

Deny permission

+

Here you can go to any permission and change the value from Allow to Deny or vice versa.

+

The second way to do this is with the adb shell command:

+

adb shell pm revoke package_name permission_name

+

For our application, the command will look like this:

+

adb shell pm revoke com.kaspersky.kaspresso.tutorial android.permission.CALL_PHONE

+

After executing the command, the application will ask for permission again the next time you try to make a call.

+

Create a test

+

When testing applications that require permissions, there are certain considerations. Let's write a test for this screen.

+

First of all, let's create a Page Object of the screen with the Make Call button

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.edit.KEditText
+import io.github.kakaocup.kakao.text.KButton
+
+object MakeCallActivityScreen : KScreen<MakeCallActivityScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val inputNumber = KEditText { withId(R.id.input_number) }
+    val makeCallButton = KButton { withId(R.id.make_call_btn) }
+}
+
+

To get to this screen, you will need to click on the corresponding button in MainActivity, add this button to MainScreen

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.text.KButton
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
+    val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }
+    val loginActivityButton = KButton { withId(R.id.login_activity_btn) }
+    val notificationActivityButton = KButton { withId(R.id.notification_activity_btn) }
+    val makeCallActivityButton = KButton { withId(R.id.make_call_activity_btn) }
+}
+
+

We can create a test. For now, let's just open the screen for making a call, enter some number and click on the button

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
+import org.junit.Rule
+import org.junit.Test
+
+class MakeCallActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkSuccessCall() = run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText("111")
+                makeCallButton.click()
+            }
+        }
+    }
+}
+
+

Let's run the test. Test passed successfully.

+

Depending on whether you have given permission or not, you may see a dialog asking permission to make calls.

+

At this stage, we have checked the operation of our screen, that it is possible to enter a number and click on the button, but we have not checked in any way whether a call is being made to the entered number or not. To check if a call is currently in progress, you can use AudioManager, this is done as follows:

+
val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
+
+

We can add this check in a separate step:

+
package com.kaspersky.kaspresso.tutorial
+
+import android.media.AudioManager
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class MakeCallActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkSuccessCall() = run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText("111")
+                makeCallButton.click()
+            }
+        }
+        step("Check phone is calling") {
+            val manager = device.context.getSystemService(AudioManager::class.java)
+            Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
+        }
+    }
+}
+
+
+

Info

+
+

Before running the test, remove the application from the device or revoke permissions using the adb shell command. Also make sure you are running the test on a device with API 23 and higher.

+

Let's run the test. Test failed.

+

This happened because after clicking on the button, the user was asked for permission. No one gave this permission, and the next screen was not opened.

+

Testing with the TestRule

+

There are several options for solving the problem. The first option is to use GrantPermissionRule. The essence of this method is that we create a list of permissions that will be automatically allowed on the device under test.

+

To do this, we add a new rule before the test method:

+
@get:Rule
+val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+    android.Manifest.permission.CALL_PHONE
+)
+
+

In the grant method, in parentheses, we list all the required permissions separated by commas, in this case there is only one, so we leave it as it is. Then the whole test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.test.ext.junit.rules.activityScenarioRule
+import androidx.test.rule.GrantPermissionRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class MakeCallActivityTest : TestCase() {
+
+    @get:Rule
+    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        android.Manifest.permission.CALL_PHONE
+    )
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkSuccessCall() = run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText("111")
+                makeCallButton.click()
+            }
+        }
+        step("Check phone is calling") {
+            val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+            Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
+        }
+    }
+}
+
+
+

Info

+
+

Remember to revoke all permissions from the app or remove it from the device before running the test.

+

Let's run the test. In some cases, this test will pass, and in others it will not. We will now analyze the reason.

+

FlakySafely for assertions

+

Remember the lesson about the flakySafely method. There we talked about the fact that in case of failure, all checks in Kaspresso will be restarted within a certain timeout.

+

In our case, we start the call and the next step is to check that the phone is really ringing. We do this through the Assert.assertTrue(…) method. Sometimes the device manages to dial the number before this check, and sometimes it does not. It seems that in such a situation the flakySafely method should work and the check should be carried out again within ten seconds, but for some reason this does not happen.

+

The fact is that all checks of view-elements in Kaspresso (isVisible, isClickable ...) "under the hood" use the flakySafely method, but if we ourselves call various checks through assert, then flakySafely will not be used and if the check fails, the test will immediately finished with failure.

+

Cases like this are another example of when you should explicitly call flakySafely

+

package com.kaspersky.kaspresso.tutorial
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.test.ext.junit.rules.activityScenarioRule
+import androidx.test.rule.GrantPermissionRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class MakeCallActivityTest : TestCase() {
+
+    @get:Rule
+    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        android.Manifest.permission.CALL_PHONE
+    )
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkSuccessCall() = run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText("111")
+                makeCallButton.click()
+            }
+        }
+        step("Check phone is calling") {
+            flakySafely {
+                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
+            }
+        }
+    }
+}
+
+Now the test works, but it has several problems.

+

Firstly, after the end of the test, the call to the subscriber is still ongoing on the device. Let's add the before and after sections and in the section that runs after the test, complete the call. This can be done with the following code: device.phone.cancelCall("111"). This method works through adb commands, so do not forget to start the adb server.

+

Theoretically, you could put the call reset in a separate step and run it as the last step without moving it to the after section. But this would be a bad decision, because if any step fails and the test fails, then the device will continue the call and never reset. The advantage of the after section is that the code inside this block will be executed regardless of the result of the test.

+

In order not to duplicate the same number in two places, let's move it to a separate variable, then the test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.test.ext.junit.rules.activityScenarioRule
+import androidx.test.rule.GrantPermissionRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class MakeCallActivityTest : TestCase() {
+
+    @get:Rule
+    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        android.Manifest.permission.CALL_PHONE
+    )
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    private val testNumber = "111"
+
+    @Test
+    fun checkSuccessCall() = before {
+    }.after {
+        device.phone.cancelCall(testNumber)
+    }.run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText(testNumber)
+                makeCallButton.click()
+            }
+        }
+        step("Check phone is calling") {
+            flakySafely {
+                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
+            }
+        }
+    }
+}
+
+

Now, after the test is completed, the call ends.

+

The second problem is that when using GrantPermissionRule we can only check the application in the state where the user has given the permission. At the same time, it is possible that the developers did not foresee the option when the permission request was rejected, then the result may be unexpected up to the point that the application will crash. We need to check these scenarios too, but using GrantPermissionRule for this will not work, because in this case the permission will always be approved, and in tests we will never know what the behavior will be if the request is denied.

+

Testing with Device.Permissions

+

One of the solutions to the problem is to interact with the dialog using KAutomator, having previously found all the necessary interface elements, but this is not very convenient, and a much more convenient way has been added to the Kaspresso - Device.Permissions. It makes it very easy to check permission dialogs, as well as accept or reject them.

+

Therefore, instead of Rule we will use the Permissions object, which can be obtained from Device. Let's do this in a separate class so that you can keep both test cases. The class in which we are currently working will be renamed to MakeCallActivityRuleTest.

+

To do this, right-click on the file name and select Refactor -> Rename

+

Rename

+

And enter a new class name:

+

Rename

+

And create a new class MakeCallActivityDevicePermissionsTest. Code can be copied from the current test, except for GrantPermissionRule

+
package com.kaspersky.kaspresso.tutorial
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class MakeCallActivityDevicePermissionsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    private val testNumber = "111"
+
+    @Test
+    fun checkSuccessCall() = before {
+    }.after {
+        device.phone.cancelCall(testNumber)
+    }.run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText(testNumber)
+                makeCallButton.click()
+            }
+        }
+        step("Check phone is calling") {
+            flakySafely {
+                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
+            }
+        }
+    }
+}
+
+

If we run the test now, it will fail because we do not have needed permission to make calls. Let's add one more step in which we will give the appropriate permission through device.permissions. After specifying an object, you can put a dot and see what methods it has:

+

Device permission methods

+

It is possible to check if the dialog is displayed, as well as to reject or grant permission.

+
step("Accept permission") {
+    Assert.assertTrue(device.permissions.isDialogVisible())
+    device.permissions.allowViaDialog()
+}
+
+

In this way, we will make sure that the dialog is displayed and agree to making calls.

+
+

Info

+
+

As a reminder, the dialog will be shown on Android API version 23 and above, how to run these tests on earlier versions, we will explain at the end of this tutorial.

+

Here we have written device.permissions twice, let's shorten the code a bit by using the apply function. And let's move the check through assert to the flakySafely method. Then the whole test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class MakeCallActivityDevicePermissionsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    private val testNumber = "111"
+
+    @Test
+    fun checkSuccessCall() = before {
+    }.after {
+        device.phone.cancelCall(testNumber)
+    }.run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText(testNumber)
+                makeCallButton.click()
+            }
+        }
+        step("Accept permission") {
+            device.permissions.apply {
+                flakySafely {
+                    Assert.assertTrue(isDialogVisible())
+                    allowViaDialog()
+                }
+            }
+        }
+        step("Check phone is calling") {
+            flakySafely {
+                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
+            }
+        }
+    }
+}
+
+

Let's run the test. Test passed successfully.

+

Now we can easily write a test for the fact that the call is not made if permission was not given. To do this, instead of allowViaDialog you need to specify denyViaDialog.

+

You also need to change the checks in the test itself, and do not forget to remove the code from the after function in the new method, since after the permission is denied, the call will not be made, and after the test, you no longer need to reset the call.

+
package com.kaspersky.kaspresso.tutorial
+
+import android.content.Context
+import android.media.AudioManager
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class MakeCallActivityDevicePermissionsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    private val testNumber = "111"
+
+    @Test
+    fun checkSuccessCall() = before {
+    }.after {
+        device.phone.cancelCall(testNumber)
+    }.run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText(testNumber)
+                makeCallButton.click()
+            }
+        }
+        step("Accept permission") {
+            device.permissions.apply {
+                flakySafely {
+                    Assert.assertTrue(isDialogVisible())
+                    allowViaDialog()
+                }
+            }
+        }
+        step("Check phone is calling") {
+            flakySafely {
+                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
+            }
+        }
+    }
+
+    @Test
+    fun checkCallIfPermissionDenied() = run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText(testNumber)
+                makeCallButton.click()
+            }
+        }
+        step("Deny permission") {
+            device.permissions.apply {
+                flakySafely {
+                    Assert.assertTrue(isDialogVisible())
+                    denyViaDialog()
+                }
+            }
+        }
+        step("Check stay on the same screen") {
+            MakeCallActivityScreen {
+                inputNumber.isDisplayed()
+                makeCallButton.isDisplayed()
+            }
+        }
+    }
+}
+
+

Testing against different API versions

+

On modern versions of the Android OS (API 23 and higher), permissions are requested from the user during the application through a dialog. But in earlier versions, they were requested at the time of installation of the application, and during operation it was considered that the user agreed with all the required permissions.

+

Therefore, if you run the test on devices with API below version 23, then there will be no request for permissions, so the dialog check is not required.

+

In the test using GrantPermissionRule no changes are required, on older versions the permission is always there, so this annotation will not affect the test in any way. But in the test using device.permissions, changes need to be made, because here we are explicitly checking the operation of the dialog.

+

There are several options here. Firstly, on such devices it makes no sense to test the application if the permission was denied, so this test should simply be skipped. To do this, you can use the @SuppressSdk annotation. Then the code of the checkCallIfPermissionDenied method will change to:

+
@SdkSuppress(minSdkVersion = 23)
+@Test
+fun checkCallIfPermissionDenied() = run {
+    step("Open make call activity") {
+        MainScreen {
+            makeCallActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+    }
+    step("Check UI elements") {
+        MakeCallActivityScreen {
+            inputNumber.isVisible()
+            inputNumber.hasHint(R.string.phone_number_hint)
+            makeCallButton.isVisible()
+            makeCallButton.isClickable()
+            makeCallButton.hasText(R.string.make_call_btn)
+        }
+    }
+    step("Try to call number") {
+        MakeCallActivityScreen {
+            inputNumber.replaceText(testNumber)
+            makeCallButton.click()
+        }
+    }
+    step("Deny permission") {
+        device.permissions.apply {
+            flakySafely {
+                Assert.assertTrue(isDialogVisible())
+                denyViaDialog()
+            }
+        }
+    }
+    step("Check stay on the same screen") {
+        MakeCallActivityScreen {
+            inputNumber.isDisplayed()
+            makeCallButton.isDisplayed()
+        }
+    }
+}
+
+

Now this test will be performed only on new versions of the Android OS, and on older versions it will be skipped.

+

The second solution for the problem is to skip certain steps or replace them with others, depending on the API level. For example, in the checkSuccessCall method on old devices, we can skip the step with checking the dialog, for this use the following code:

+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+    step("Accept permission") {
+        device.permissions.apply {
+            flakySafely {
+                Assert.assertTrue(isDialogVisible())
+                allowViaDialog()
+            }
+        }
+    }
+}
+
+

The rest of the code can be left untouched and the test will run successfully on both new and old devices, just in one case permission will be requested, in the other it won't.

+

The final test code will now look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import android.content.Context
+import android.media.AudioManager
+import android.os.Build
+import androidx.test.ext.junit.rules.activityScenarioRule
+import androidx.test.filters.SdkSuppress
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.MakeCallActivityScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class MakeCallActivityDevicePermissionsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    private val testNumber = "111"
+
+    @Test
+    fun checkSuccessCall() = before {
+    }.after {
+        device.phone.cancelCall(testNumber)
+    }.run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText(testNumber)
+                makeCallButton.click()
+            }
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            step("Accept permission") {
+                device.permissions.apply {
+                    flakySafely {
+                        Assert.assertTrue(isDialogVisible())
+                        allowViaDialog()
+                    }
+                }
+            }
+        }
+        step("Check phone is calling") {
+            flakySafely {
+                val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+                Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 23)
+    @Test
+    fun checkCallIfPermissionDenied() = run {
+        step("Open make call activity") {
+            MainScreen {
+                makeCallActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check UI elements") {
+            MakeCallActivityScreen {
+                inputNumber.isVisible()
+                inputNumber.hasHint(R.string.phone_number_hint)
+                makeCallButton.isVisible()
+                makeCallButton.isClickable()
+                makeCallButton.hasText(R.string.make_call_btn)
+            }
+        }
+        step("Try to call number") {
+            MakeCallActivityScreen {
+                inputNumber.replaceText(testNumber)
+                makeCallButton.click()
+            }
+        }
+        step("Deny permission") {
+            device.permissions.apply {
+                flakySafely {
+                    Assert.assertTrue(isDialogVisible())
+                    denyViaDialog()
+                }
+            }
+        }
+        step("Check stay on the same screen") {
+            MakeCallActivityScreen {
+                inputNumber.isDisplayed()
+                makeCallButton.isDisplayed()
+            }
+        }
+    }
+}
+
+

Summary

+

In this tutorial, we have looked at two options for working with Permissions: GrantPermissionRule and device.permissions.

+

We also learned that the second option is preferable for a number of reasons:

+
    +
  1. The Permissions object makes it possible to test whether a dialog requesting permission is displayed
  2. +
  3. When using Permissions, we can test the application's behavior not only when accepting a permission, but also when denying it
  4. +
  5. Tests with the GrantPermissionRule will fail if the permission was previously denied. You will need to reinstall the application or cancel previously issued permissions through the adb shell command
  6. +
  7. If you revoke the permission using the adb shell command while the test is running, then the test will work correctly if the Permissions object is used, but a crash will occur if the GrantPermissionRule is used
  8. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Download_Kaspresso_project_and_Android_studio/index.html b/Tutorial/Download_Kaspresso_project_and_Android_studio/index.html new file mode 100644 index 000000000..9e6cfa2b6 --- /dev/null +++ b/Tutorial/Download_Kaspresso_project_and_Android_studio/index.html @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 2. Download Kaspresso project and Android studio - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Setting up the required environment.

+

In this lesson, we will download the Kaspresso project, install Android studio and set up the emulator.

+

Download Android Studio

+

Android Studio is used for software development. We will need it to write and run autotests. +
If you already have Android Studio installed, skip this step. If not, then follow the link and click Download Android Studio.

+


Run the downloaded file and go through all the steps of the initial setup of the studio. You can use the official manual or the official codelabs manual in case of problems. +
After Android Studio is downloaded, run it.

+

Downloading the Kaspresso project

+

To download a project, you must have the GIT version control system installed on your computer. You can download GIT and learn more about it here.

+

Once GIT is installed, you will be able to download the project. To do this, follow the link.

+

Click the Code button and copy the link to the repository

+

Download Kaspresso button

+

Open Android Studio.

+

If you have not previously opened any project in the studio, then you must select the Get From VCS item

+

Get Project from VCS

+

If a project has already been launched, then you can load a new one from GIT as follows: File -> New -> Project From Version Control

+

Get Project from VCS

+

In the window that opens, enter the copied project URL, select the folder where Kaspresso will be placed and click clone.

+

Clone Project

+

Setting up the emulator.

+

In the top menu of Android Studio, select 'Tools' -> 'Device Manager'

+

Tools Device Manager

+

The tab for managing emulators and real devices will appear on the screen. Click on 'Create Device':

+

Create Device

+

We will see the following screen:

+

Select hardware

+

On this screen, you can set the characteristics of the hardware you want to emulate. In section "1" you can select phone, tablet, TV and so on. For the purposes of this tutorial we will be working with the "phone" type. In section "2" you can select a specific model. Within the scope of this guide, it makes no difference which one to choose. Choose 'Pixel 6'. Click 'Next' and get to the operating system image selection window:

+

System image

+

This screen is more important for regular work and lets you choose which version of Android to install on the emulator. Let's choose 'R'. Click on the download icon to the right of the letter 'R', go through the installation process and wait.

+

SDK_component_isntaller

+

When the installation process is completed, click the Finish button:

+

SDK_component_isntaller_finish

+

Select the installed version ('R') and click 'Next':

+

SDK_component_installer_next

+

On the screen below, you can change the name of the created emulator so that it is easy to distinguish between them. The default value is fine for our purposes. Click 'Finish'.

+

Device_name

+

The device is set up and ready for work. We launch it by the 'Play' icon to the right of the device name:

+

Launch_device

+

In some cases, Android Studio may recommend installing Hypervisor:

+

Hyper_Visor

+

Hyper_Visor_next

+

Summary

+

Android Studio is installed, emulator is configured, Kaspresso project is loaded. In the next lesson, we will run the first tests.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/FlakySafely/index.html b/Tutorial/FlakySafely/index.html new file mode 100644 index 000000000..1ce141315 --- /dev/null +++ b/Tutorial/FlakySafely/index.html @@ -0,0 +1,1996 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 9. flakySafely - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Flaky Safely. Testing with timeout

+

In this tutorial, we'll learn how to test screens that change state over time.

+

So far, in all tests, the screens immediately had a final look, all elements were displayed when they were opened, and we could conduct tests. To change the status, we ourselves performed some actions - clicked on the button, entered text in the input field, and so on.

+

But often there is a situation where the appearance of the screen changes over time. For example, at the start, data loading begins - a ProgressBar is displayed, after loading, a list of elements or an error dialog is displayed if something went wrong. In such cases, during the test, you need to check all intermediate states, while not changing them from the test method.

+

Consider an example. Open the tutorial application and click on the Flaky Activity button

+

Flaky activity button

+

This screen displays several TextView for which some data is being loaded

+

Flaky screen 1

+

After one second, the text for the first element is loaded

+

Flaky screen 2

+

After another three seconds, text appears on the second element

+

Flaky screen 3

+

After 10 seconds, the rest of the data will be loaded and the texts will appear in all TextView

+

Flaky screen 4

+

Testing FlakyScreen

+

Let's write a test for this screen. As usual, let's start by creating a Page Object

+

package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.progress.KProgressBar
+import io.github.kakaocup.kakao.text.KButton
+
+object FlakyScreen : KScreen<FlakyScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val text1 = KButton { withId(R.id.text_1) }
+    val text2 = KButton { withId(R.id.text_2) }
+    val text3 = KButton { withId(R.id.text_3) }
+    val text4 = KButton { withId(R.id.text_4) }
+    val text5 = KButton { withId(R.id.text_5) }
+
+    val progressBar1 = KProgressBar { withId(R.id.progress_bar_1) }
+    val progressBar2 = KProgressBar { withId(R.id.progress_bar_2) }
+    val progressBar3 = KProgressBar { withId(R.id.progress_bar_3) }
+    val progressBar4 = KProgressBar { withId(R.id.progress_bar_4) }
+    val progressBar5 = KProgressBar { withId(R.id.progress_bar_5) }
+}
+
+To go to FlakyActivity you need to click the button on the main screen. Let's add it to PageObject MainScreen

+

package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.text.KButton
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
+    val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }
+    val loginActivityButton = KButton { withId(R.id.login_activity_btn) }
+    val notificationActivityButton = KButton { withId(R.id.notification_activity_btn) }
+    val makeCallActivityButton = KButton { withId(R.id.make_call_activity_btn) }
+    val flakyActivityButton = KButton { withId(R.id.flaky_activity_btn) }
+}
+
+Let's first check that the screen is open, all elements are visible and the ProgressBar is displayed on them

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.FlakyScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class FlakyScreenTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkFlakyScreen() = run {
+        step("Open flaky screen") {
+            MainScreen {
+                flakyActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check initial elements") {
+            FlakyScreen {
+                text1.isVisible()
+                text2.isVisible()
+                text3.isVisible()
+                text4.isVisible()
+                text5.isVisible()
+                progressBar1.isVisible()
+                progressBar2.isVisible()
+                progressBar3.isVisible()
+                progressBar4.isVisible()
+                progressBar5.isVisible()
+            }
+        }
+    }
+}
+
+

The next action that happens on the screen is loading the text for the first element. We need to check that at this stage the first TextView contains the text "TEXT 1". This check must be done after the download is complete.

+

It turns out that the next step is to add the necessary checks, and if they fail, then we need to perform them again for some time. In this case, loading the first text takes about one second after opening the screen, so we can add a timeout of 1-3 seconds, during which the checks will be repeated. If during this time the methods return the correct value, then the test will complete successfully, but if after the timeout the condition is not met, then the test will fail.

+

In order to add a timeout, you must use the flakySafely method, where the time in milliseconds is indicated in parentheses during which attempts to pass the test will occur. Then the test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.FlakyScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class FlakyScreenTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkFlakyScreen() = run {
+        step("Open flaky screen") {
+            MainScreen {
+                flakyActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check initial elements") {
+            FlakyScreen {
+                text1.isVisible()
+                text2.isVisible()
+                text3.isVisible()
+                text4.isVisible()
+                text5.isVisible()
+                progressBar1.isVisible()
+                progressBar2.isVisible()
+                progressBar3.isVisible()
+                progressBar4.isVisible()
+                progressBar5.isVisible()
+            }
+        }
+        step("Check first element after loading") {
+            FlakyScreen {
+                flakySafely(3000) {
+                    text1.hasText(R.string.text_1)
+                    progressBar1.isGone() // Проверяем, что ProgressBar невидимый
+                }
+            }
+        }
+    }
+}
+
+

Let's launch the test. It passed successfully.

+

When to use flakySafely

+

Our test completes successfully. Now let's check what happens if we remove the call to the flakySafely method

+

package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.FlakyScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class FlakyScreenTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkFlakyScreen() = run {
+        step("Open flaky screen") {
+            MainScreen {
+                flakyActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check initial elements") {
+            FlakyScreen {
+                text1.isVisible()
+                text2.isVisible()
+                text3.isVisible()
+                text4.isVisible()
+                text5.isVisible()
+                progressBar1.isVisible()
+                progressBar2.isVisible()
+                progressBar3.isVisible()
+                progressBar4.isVisible()
+                progressBar5.isVisible()
+            }
+        }
+        step("Check first element after loading") {
+            FlakyScreen {
+                text1.hasText(R.string.text_1)
+                progressBar1.isGone() // Проверяем, что ProgressBar невидимый
+            }
+        }
+    }
+}
+
+Let's launch the test. It still succeeds.

+

It would seem that we did not set any timeout, the check should have failed, but the test is green. The fact is that in Kaspresso all checks implicitly use the flakySafely method with some kind of timeout (in the current version of Kaspresso, the timeout is 10 seconds).

+

You may have noticed that if a test runs successfully, the application closes immediately and Android Studio displays a message that the tests ran successfully. But if some check fails, then the error message does not appear immediately, but after a few seconds - the reason lies in the use of flakySafely. The test fails and restarts several more times within 10 seconds.

+

Therefore, flakySafely should be added only if the default timeout does not suit you for some reason, and you need to change it to another one. A good use case for the extended timeout is when the screen is loading data from the network. The server may take a long time to return a response, while the test should not fall due to a slow backend.

+

In the next step, after 3 seconds, the second text is loaded. Three seconds is within the default timeout, so explicitly using flakeSafely with a different timeout doesn't make sense.

+

package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.FlakyScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class FlakyScreenTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkFlakyScreen() = run {
+        step("Open flaky screen") {
+            MainScreen {
+                flakyActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check initial elements") {
+            FlakyScreen {
+                text1.isVisible()
+                text2.isVisible()
+                text3.isVisible()
+                text4.isVisible()
+                text5.isVisible()
+                progressBar1.isVisible()
+                progressBar2.isVisible()
+                progressBar3.isVisible()
+                progressBar4.isVisible()
+                progressBar5.isVisible()
+            }
+        }
+        step("Check first element after loading") {
+            FlakyScreen {
+                text1.hasText(R.string.text_1)
+                progressBar1.isGone()
+            }
+        }
+        step("Check second element after loading") {
+            FlakyScreen {
+                text2.hasText(R.string.text_2)
+                progressBar2.isGone()
+            }
+        }
+    }
+}
+
+The next step is 10 seconds after the data for the second element is loaded, the text appears in all the other TextView. 10 seconds is an approximate data loading time, it can be more or less than this value, so the standard timeout will not work for us. In such cases, you need to explicitly call flakySafely passing an extended timeout, let's pass 15 seconds

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.FlakyScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class FlakyScreenTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkFlakyScreen() = run {
+        step("Open flaky screen") {
+            MainScreen {
+                flakyActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check initial elements") {
+            FlakyScreen {
+                text1.isVisible()
+                text2.isVisible()
+                text3.isVisible()
+                text4.isVisible()
+                text5.isVisible()
+                progressBar1.isVisible()
+                progressBar2.isVisible()
+                progressBar3.isVisible()
+                progressBar4.isVisible()
+                progressBar5.isVisible()
+            }
+        }
+        step("Check first element after loading") {
+            FlakyScreen {
+                text1.hasText(R.string.text_1)
+                progressBar1.isGone()
+            }
+        }
+        step("Check second element after loading") {
+            FlakyScreen {
+                text2.hasText(R.string.text_2)
+                progressBar2.isGone()
+            }
+        }
+        step("Check left elements after loading") {
+            FlakyScreen {
+                flakySafely(15000) {
+                    text3.hasText(R.string.text_3)
+                    progressBar3.isGone()
+                    text4.hasText(R.string.text_4)
+                    progressBar4.isGone()
+                    text5.hasText(R.string.text_5)
+                    progressBar5.isGone()
+                }
+            }
+        }
+    }
+}
+
+

Thread.sleep vs FlakySafely

+

In some tests, you may see code like Thread.sleep(delay_in_millis) used to solve timeout problems instead of flakySafely. This code stops the thread for the time that was passed as a parameter. That is, the test in this place will stop its execution and will wait for some time, after the timeout is completed, the test will continue to work.

+

At first glance, it may seem that there is no difference in these methods, and they do the same thing. But in fact, they have a significant difference. If you use flakySafely, then regardless of the timeout, the test will continue to run after a successful check. And when using Thread.sleep in any case, the test will wait until the timeout is completed.

+

Normally, all checks in Kaspresso use flakySafely with a timeout of 10 seconds, but despite this, the tests complete very quickly, because if the method returned the correct value, then there will be no waiting. If all these methods are replaced by Thread.sleep, then each such check will take at least 10 seconds and the tests will run for a very long time.

+

What timeout to specify?

+

Knowing the benefits of flakySafely that we just discussed, you may want to specify a very large timeout for all tests, just to be on the safe side. But this should not be done for several reasons.

+

Firstly, if the application really does not work correctly, and some tests will fail, then their passage will be much longer than with a standard timeout.

+

Secondly, there may be some bugs in the application that cause it to run much slower than expected. In this case, we could learn about the problem from autotests, but if the timeout is too long, it will go unnoticed.

+

Therefore, in most cases, the standard timeout will suit you, and you do not need to explicitly specify it. Otherwise, specify a timeout that is acceptable to the user.

+

Features of working with ScrollView

+

You may have noticed that all the elements on the screen do not fit, because they take up quite a lot of space in height, so all the content was added to the ScrollView, so that the screen can be scrolled.

+

We can add a check that when the screen is opened, the first element is displayed, but the last one is not. It would be wrong to use the isVisible method in this case, because even if the object does not fit on the screen, but it is visible, the check will return true. Instead, you can use the isDisplayed and isNotDisplayed methods, which are needed just in such cases - when you need to know that the element is actually visible on the screen.

+

Then the test code will look like this:

+

package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.FlakyScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class FlakyScreenTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkFlakyScreen() = run {
+        step("Open flaky screen") {
+            MainScreen {
+                flakyActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check display of elements") {
+            FlakyScreen {
+                text1.isDisplayed()
+                text5.isNotDisplayed()
+            }
+        }
+        step("Check initial elements") {
+            FlakyScreen {
+                text1.isVisible()
+                text2.isVisible()
+                text3.isVisible()
+                text4.isVisible()
+                text5.isVisible()
+                progressBar1.isVisible()
+                progressBar2.isVisible()
+                progressBar3.isVisible()
+                progressBar4.isVisible()
+                progressBar5.isVisible()
+            }
+        }
+        step("Check first element after loading") {
+            FlakyScreen {
+                text1.hasText(R.string.text_1)
+                progressBar1.isGone()
+            }
+        }
+        step("Check second element after loading") {
+            FlakyScreen {
+                text2.hasText(R.string.text_2)
+                progressBar2.isGone()
+            }
+        }
+        step("Check left elements after loading") {
+            FlakyScreen {
+                flakySafely(15000) {
+                    text3.hasText(R.string.text_3)
+                    progressBar3.isGone()
+                    text4.hasText(R.string.text_4)
+                    progressBar4.isGone()
+                    text5.hasText(R.string.text_5)
+                    progressBar5.isGone()
+                }
+            }
+        }
+    }
+}
+
+Test passed successfully. Now let's change the check for the fifth element of the list. Now instead of the isNotDisplayed method, we use isDisplayed.

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.FlakyScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class FlakyScreenTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkFlakyScreen() = run {
+        step("Open flaky screen") {
+            MainScreen {
+                flakyActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check display of elements") {
+            FlakyScreen {
+                text1.isDisplayed()
+                text5.isDisplayed()
+            }
+        }
+        step("Check initial elements") {
+            FlakyScreen {
+                text1.isVisible()
+                text2.isVisible()
+                text3.isVisible()
+                text4.isVisible()
+                text5.isVisible()
+                progressBar1.isVisible()
+                progressBar2.isVisible()
+                progressBar3.isVisible()
+                progressBar4.isVisible()
+                progressBar5.isVisible()
+            }
+        }
+        step("Check first element after loading") {
+            FlakyScreen {
+                text1.hasText(R.string.text_1)
+                progressBar1.isGone()
+            }
+        }
+        step("Check second element after loading") {
+            FlakyScreen {
+                text2.hasText(R.string.text_2)
+                progressBar2.isGone()
+            }
+        }
+        step("Check left elements after loading") {
+            FlakyScreen {
+                flakySafely(15000) {
+                    text3.hasText(R.string.text_3)
+                    progressBar3.isGone()
+                    text4.hasText(R.string.text_4)
+                    progressBar4.isGone()
+                    text5.hasText(R.string.text_5)
+                    progressBar5.isGone()
+                }
+            }
+        }
+    }
+}
+
+

It seems that the test should fail, since initially the fifth element is not visible on the screen. We launch. Test passed successfully.

+

The reason for this behavior is the implementation of checks in the Kaspresso library. If we test an element that is inside ScrollView and this test fails, then the test will automatically scroll to that element, and the test will will be executed again. Thus, the problem was solved when, during the normal behavior of the application, the tests crashed only because they could not check an element that is not currently visible on the screen.

+

It turns out that the text5.isDisplayed check was performed, it failed and the screen was scrolled down and the check started again. Now the element was actually visible on the screen, so the test succeeded.

+

When writing tests for screens that can be scrolled, consider the peculiarities of working with them in Kaspresso.

+

Summary

+

In this tutorial, we covered the following points:

+
    +
  1. The `flakySafely` method for testing a stateful screen
  2. +
  3. Set different timeouts for different operations
  4. +
  5. Features of Kaspresso on scrollable screens
  6. +
  7. Difference between Thread.sleep and flakySafely
  8. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Logger_and_screenshot/index.html b/Tutorial/Logger_and_screenshot/index.html new file mode 100644 index 000000000..6d71e365d --- /dev/null +++ b/Tutorial/Logger_and_screenshot/index.html @@ -0,0 +1,1978 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 12. Logger and screenshots - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + +

Logging and screenshots

+

In this tutorial, we will learn how to identify the causes of failing tests by adding additional logs and screenshots.

+

Let's recall an example that was already used in one of the previous lessons. Opening the tutorial app

+

Tutorial main screen

+

and click on the Login Activity button

+

Login Activity

+

On this screen, you can enter your login and password, and if they are correct, the screen after authorization will open. In this case, the following are considered correct: a login with a length of three characters, a password - from six.

+

Screen after auth

+

External system for test data

+

We have already written tests for this screen, they are in the class LoginActivityTest

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.afterlogin.AfterLoginActivity
+import com.kaspersky.kaspresso.tutorial.login.LoginActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun loginSuccessfulIfUsernameAndPasswordCorrect() {
+        run {
+            step("Try to login with correct username and password") {
+                scenario(
+                    LoginScenario(
+                        username = "123456",
+                        password = "123456"
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(AfterLoginActivity::class.java)
+            }
+        }
+    }
+
+    @Test
+    fun loginUnsuccessfulIfUsernameIncorrect() {
+        run {
+            step("Try to login with incorrect username") {
+                scenario(
+                    LoginScenario(
+                        username = "12",
+                        password = "123456"
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(LoginActivity::class.java)
+            }
+        }
+    }
+
+    @Test
+    fun loginUnsuccessfulIfPasswordIncorrect() {
+        run {
+            step("Try to login with incorrect password") {
+                scenario(
+                    LoginScenario(
+                        username = "123456",
+                        password = "12345",
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(LoginActivity::class.java)
+            }
+        }
+    }
+}
+
+

In this test, we ourselves create a username and password with which we will log in. But there are times when we get the data for the test from some external system. For example, a project may have some kind of service that generates a login and password for logging in, returns it to us, and we use them for testing.

+

Let's simulate this situation. Let's create a class that returns login data - login and password.

+

Let's create another package data in the com.kaspersky.kaspresso.tutorial package

+

Create package 1

+

Create package 2

+

In the created package, add the TestData class, select the type Object

+

Create class

+

As we said earlier, here we will only simulate the situation when we receive data for the test from an external system. In the created class, we will have two methods: one of them returns the login, the other returns the password. In real projects, we would request this data from the server, and we would not have been able to change the internal implementation of the possibility. That is, now we ourselves will indicate which login and password the system will return, but we imagine that this is a “black box” for us, and we do not know what values will be received.

+

We add two methods in this class and let them return the correct login and password:

+

package com.kaspersky.kaspresso.tutorial.data
+
+object TestData {
+
+    fun generateUsername(): String = "Admin"
+
+    fun generatePassword(): String = "123456"
+}
+
+Now let's create a separate test class in which we will check for a successful login using the data received from the TestData class. Let's call the test class LoginActivityGeneratedDataTest. We can copy the successful login test from the LoginActivityTest class

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.afterlogin.AfterLoginActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityGeneratedDataTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun loginSuccessfulIfUsernameAndPasswordCorrect() {
+        run {
+            step("Try to login with correct username and password") {
+                scenario(
+                    LoginScenario(
+                        username = "123456",
+                        password = "123456"
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(AfterLoginActivity::class.java)
+            }
+        }
+    }
+}
+
+

Here we use a hardcoded username and password, let's get them from the TestData class

+

package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.afterlogin.AfterLoginActivity
+import com.kaspersky.kaspresso.tutorial.data.TestData
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityGeneratedDataTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun loginSuccessfulIfUsernameAndPasswordCorrect() {
+        run {
+            val username = TestData.generateUsername()
+            val password = TestData.generatePassword()
+            step("Try to login with correct username and password") {
+                scenario(
+                    LoginScenario(
+                        username = username,
+                        password = password
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(AfterLoginActivity::class.java)
+            }
+        }
+    }
+}
+
+We launch. Test passed successfully.

+

Analysis of failed tests

+

We checked that if the system returns correct data, then the test passes. Let's change the TestData class so that it returns incorrect values

+

package com.kaspersky.kaspresso.tutorial.data
+
+object TestData {
+
+    fun generateUsername(): String = "Adm"
+
+    fun generatePassword(): String = "123"
+}
+
+Let's run the test again. This time the test fails.

+

We have already said that in real projects we cannot influence the external system, and sometimes it can return incorrect data, which will cause the test to fail. If the test fails, then you need to analyze and determine what the problem was: in the tests, in a malfunctioning application, or in an external system. Let's try to determine this from the logs. Open Logcat and filter the log by tag KASPRESSO

+

Test failed

+

What do we see from here? The attempt to log in was successful, but the check that the correct screen was opened after a successful login failed.

+

At the same time, it is completely unclear from here why the problem arose. We do not see what data was used to log in, whether they are really correct, and it is not clear how to solve the problem that has arisen. The result would be more understandable if the logs contained information - which particular login and password were used during testing.

+

Adding logs

+

If we need to add some of our information to the logs, we can use the testLogger object, on which we need to call the i method (from the word info), and pass the text to be logged as a parameter.

+

Our login and password are generated before the step step("Try to login with correct username and password") we can display a message in the log at this point about what data was generated

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.afterlogin.AfterLoginActivity
+import com.kaspersky.kaspresso.tutorial.data.TestData
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityGeneratedDataTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun loginSuccessfulIfUsernameAndPasswordCorrect() {
+        run {
+            val username = TestData.generateUsername()
+            val password = TestData.generatePassword()
+
+            testLogger.i("Generated data. Username: $username, Password: $password")
+
+            step("Try to login with correct username and password") {
+                scenario(
+                    LoginScenario(
+                        username = username,
+                        password = password
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(AfterLoginActivity::class.java)
+            }
+        }
+    }
+}
+
+

In this line testLogger.i("Generated data. Username: $username, Password: $password") we call the i method on the testLogger object, passing the string "Generated data. Username: $username, Password: $password") as a parameter, where instead of $username and $password the values will be substituted login and password variables.

+
+

Info

+

You can read more about how to form a string using variables and methods in documentation

+
+

Let's run the test again and see the logs:

+

Custom Log

+

After TEST SECTION you can see our log, which is displayed with the KASPRESSO_TEST tag. This log shows that the generated data is incorrect (the password is too short), which means that the test fails due to an external system, and the problem needs to be solved in it.

+

If you don't want to watch the entire log, and you are only interested in messages added by you, you can filter the log by the tag KASPRESSO_TEST

+

Kaspresso test tag

+

Screenshots

+

Logs are really very useful when analyzing tests and finding bugs, but there are times when it's much easier to find a problem from screenshots. If during the test a screenshot was saved at each step, and then we could look at them in some folder, then finding errors would be much easier.

+

In Kaspresso, it is possible to take screenshots at any step during the test, for this it is enough to call the device.screenshots.take("file_name") method. Instead of file_name, you need to specify the name of the screenshot file, by which you can find it. Let's add screenshots to each LoginScenario step so that we can analyze everything that happened on the screen later.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
+import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
+import com.kaspersky.kaspresso.tutorial.screen.LoginScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+
+class LoginScenario(
+    private val username: String,
+    private val password: String
+) : Scenario() {
+
+    override val steps: TestContext<Unit>.() -> Unit = {
+        step("Open login screen") {
+            device.screenshots.take("before_open_login_screen")
+            MainScreen {
+                loginActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+            device.screenshots.take("after_open_login_screen")
+        }
+        step("Check elements visibility") {
+            device.screenshots.take("check_elements_visibility")
+            LoginScreen {
+                inputUsername {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_username)
+                }
+                inputPassword {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_password)
+                }
+                loginButton {
+                    isVisible()
+                    isClickable()
+                }
+            }
+        }
+        step("Try to login") {
+            LoginScreen {
+                inputUsername {
+                    replaceText(username)
+                    device.screenshots.take("setup_username")
+                }
+                inputPassword {
+                    replaceText(password)
+                    device.screenshots.take("setup_password")
+                }
+                loginButton {
+                    click()
+                    device.screenshots.take("after_click_login")
+                }
+            }
+        }
+    }
+}
+
+

In order for screenshots to be saved on the device, the application must have permission to read and write to the smartphone's file system. Therefore, in the test class, we will give the appropriate permission through GrantPermissionRule

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import androidx.test.rule.GrantPermissionRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.afterlogin.AfterLoginActivity
+import com.kaspersky.kaspresso.tutorial.data.TestData
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityGeneratedDataTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @get:Rule
+    val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        android.Manifest.permission.READ_EXTERNAL_STORAGE,
+        android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+    )
+
+    @Test
+    fun loginSuccessfulIfUsernameAndPasswordCorrect() {
+        run {
+            val username = TestData.generateUsername()
+            val password = TestData.generatePassword()
+
+            testLogger.i("Generated data. Username: $username, Password: $password")
+
+            step("Try to login with correct username and password") {
+                scenario(
+                    LoginScenario(
+                        username = username,
+                        password = password
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(AfterLoginActivity::class.java)
+            }
+        }
+    }
+}
+
+

Let's run the test again.

+

After running the test, go to Device File Explorer and open the sdcard/Documents/screenshots folder. If it is not displayed for you, then right-click on the sdcard folder and click Synchronize

+

Screenshots

+

Here, from the screenshots, you can determine what the problem is - at the stage of setting the password, the number of characters entered is 3

+

Setup password

+

So, after analyzing the screenshots, you can determine which error occurred at the time of the tests.

+
+

Info

+

One way to take a screenshot is to call the device.uiDevice.takeScreenshot method. This is a method from the uiautomator library and should never be used directly.

+

Firstly, a screenshot taken with Kaspresso (device.screenshots.take) will be in the correct folder, which is easy to find by the name of the test, and the files for each test and step will be in their own folders with understandable names, and in the case of uiautomator, finding the right screenshots will be problematic.

+

Secondly, Kaspresso has made a lot of convenient improvements for working with screenshots, such as scaling, photo quality settings, full-screen screenshots (when all the content does not fit on the screen), and so on.

+

Therefore, for screenshots, always use only the Kaspresso device.screenshots objects.

+
+

Setting up Kaspresso.Builder

+

Theoretically, all tests you write can fail. In such cases, I would like to always be able to look at screenshots to understand what went wrong. How to achieve this? As an option, add a method call that takes a screenshot to all steps of all tests, but this is not very convenient.

+

Therefore, Kaspresso has added the ability to configure test parameters when creating a test class. To do this, you can pass the Kaspresso.Builder object to the TestCase constructor, which by default takes the value Kaspresso.Builder.simple().

+

Test Case Params

+
+

Info

+

To see the parameters a method or constructor takes, you can left-click inside the parentheses and press ctrl + P (or cmd + P on Mac)

+
+

We can add many different settings, you can read more about them in the Wiki.

+

Now we are interested in adding screenshots if the tests have failed. The easiest way to do this is to use advanced builder instead of simple. This is done as follows:

+

class LoginActivityGeneratedDataTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.advanced()
+)
+
+In this case, the call to methods that take screenshots can be removed, they will be saved automatically if the test fails.

+
+

Info

+

Please note that permissions to access the file system are required, without them screenshots will not be saved.

+
+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
+import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
+import com.kaspersky.kaspresso.tutorial.screen.LoginScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+
+class LoginScenario(
+    private val username: String,
+    private val password: String
+) : Scenario() {
+
+    override val steps: TestContext<Unit>.() -> Unit = {
+        step("Open login screen") {
+            MainScreen {
+                loginActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check elements visibility") {
+            LoginScreen {
+                inputUsername {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_username)
+                }
+                inputPassword {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_password)
+                }
+                loginButton {
+                    isVisible()
+                    isClickable()
+                }
+            }
+        }
+        step("Try to login") {
+            LoginScreen {
+                inputUsername {
+                    replaceText(username)
+                }
+                inputPassword {
+                    replaceText(password)
+                }
+                loginButton {
+                    click()
+                }
+            }
+        }
+    }
+}
+
+

Let's start the test. Tests failed and screenshots appeared on the device (don't forget to press Synchronize):

+

Advanced Builder

+

When using the advanced builder, there are a few more changes. In addition to screenshots, files with logs, the View hierarchy, and more are also added.

+

If you do not need all these changes, then you can only change certain settings of a simple builder.

+
+

Info

+

If you're not a developer, customizing the default builder can be quite tricky. In case it was not possible to figure out the setting, use the advanced builder to get screenshots

+
+

Interceptors

+

You should remember that in the previous tests, in addition to executing our methods, there were many additional actions “under the hood”: writing logs for each step, implicitly calling flakySafely, automatically scrolling to the element if the check was unsuccessful, and so on.

+

All this worked thanks to Interceptors. Interceptors are classes that intercept the actions we call and add some functionality to them. There are a lot of such classes in Kaspresso, you can read more about them in documentation

+

We are interested in adding screenshots, the ScreenshotStepWatcherInterceptor, ScreenshotFailStepWatcherInterceptor and TestRunnerScreenshotWatcherInterceptor classes are responsible for this.

+
    +
  • ScreenshotStepWatcherInterceptor - adds screenshots whether the step failed or not +
  • +
  • ScreenshotFailStepWatcherInterceptor - adds a screenshot of only the step that failed +
  • +
  • TestRunnerScreenshotWatcherInterceptor - adds a screenshot if an error occurs in the `before` or `after` section +
  • +
+ +

If the test fails, it is convenient to look not only at the step at which the error occurred, but also at the previous ones - this way it is much easier to figure out the problem. Therefore, we will add the first Interceptor option, which will screenshot all the steps, regardless of the result. This is done as follows:

+

class LoginActivityGeneratedDataTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple().apply {
+        stepWatcherInterceptors.add(ScreenshotStepWatcherInterceptor(screenshots))
+    }
+)
+
+Here we first get the default builder, call its apply method, and add all the necessary settings in curly braces. In this case, we get all the Interceptors that intercept the step event (step) and add a ScreenshotStepWatcherInterceptor there, passing the screenshots object to the constructor.

+

Now that we have added this Interceptor, after each test step, regardless of the result of its execution, screenshots will be saved on the device.

+

We launch. The test failed and screenshots were saved to the device

+

Customized Builder

+

Let's return the correct implementation of the TestData class

+
package com.kaspersky.kaspresso.tutorial.data
+
+object TestData {
+
+    fun generateUsername(): String = "Admin"
+
+    fun generatePassword(): String = "123456"
+}
+
+

Let's run the test again. The test passed successfully and all screenshots are saved on the device.

+

Summary

+

In this tutorial, we learned how to add logging and screenshots to our tests. We found out when standard logs are not enough, learned how to customize Kaspresso.Builder by adding various Interceptors to it. +We also looked at ways to create screenshots manually, and how this process can be automated.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Logger_and_screenshots/index.html b/Tutorial/Logger_and_screenshots/index.html new file mode 100644 index 000000000..79636f1db --- /dev/null +++ b/Tutorial/Logger_and_screenshots/index.html @@ -0,0 +1,1321 @@ + + + + + + + + + + + + + + + + + + + + + Logger and screenshots - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Logger and screenshots

+ + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Recyclerview/index.html b/Tutorial/Recyclerview/index.html new file mode 100644 index 000000000..fc4cba824 --- /dev/null +++ b/Tutorial/Recyclerview/index.html @@ -0,0 +1,2158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 11. RecyclerView. Testing list of elements - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

RecyclerView. Testing list of elements

+

In practice, we often have to work with screens that contain lists of elements, and these lists are dynamic, and their size and content can change. When testing such screens, there are some peculiarities. We will talk about them in this lesson.

+

Open the tutorial application and click on the List Activity button.

+

Main Screen

+

You will see the following screen:

+

Todo List

+

It displays the user's to-do list. Each element of the list has a serial number, text and color, which is set depending on the priority. If the priority is low, then the background color is green, if medium, then orange, if high, then red.

+

It is also possible to delete list items with a swipe action.

+

Swipe element

+

Remove element

+

Let's write tests for this screen. We need the IDs of the list elements, we will use the LayoutInspector to find them.

+

Layout Inspector

+

Note that all list items are inside RecyclerView with id rv_notes. The recycler has three objects that have the same IDs: note_container, tv_note_id and tv_note_text.

+

It turns out that we will not be able to test the screen in the usual way, since all elements have the same ID, instead we use a different approach. The PageObject of the screen with the list of notes will contain only one element - RecyclerView, and the elements of the list will be separate PageObjects, whose content we will check.

+

Let's start writing a test. First of all let's add PageObject NoteListScreen.

+

package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.recycler.KRecyclerView
+
+object NoteListScreen : KScreen<NoteListScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val rvNotes = KRecyclerView { withId(R.id.rv_notes) }
+}
+
+If we write such code, then we will have some errors. The fact is that if you are testing a RecyclerView, then it is assumed that you will be checking the elements of the list, and not the container with these elements. Therefore, when creating an instance of KRecyclerView, it is not enough to pass only the matcher by which the object will be found, you must pass the second parameter, which is called itemTypeBuilder.

+
+

Info

+

If you want to know what parameters to pass to a particular method or constructor, you can press the key combination ctrl + P (cmd + P on Mac OS), and you will see a tooltip that will indicate the necessary arguments.

+
+

We have already said earlier that we will need a Page Object for each list item, so we need to create an appropriate class, we will pass an instance of this class to itemTypeBuilder.

+

In the same file, add the NoteItemScreen class, this time we inherit not from KScreen, but from KRecyclerViewItem, since now it is not a regular Page Object, but a list item RecyclerView

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.recycler.KRecyclerItem
+import io.github.kakaocup.kakao.recycler.KRecyclerView
+
+object NoteListScreen : KScreen<NoteListScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val rvNotes = KRecyclerView { withId(R.id.rv_notes) }
+
+    class NoteItemScreen: KRecyclerItem<NoteItemScreen>() {
+
+    }
+}
+
+

Please note that earlier when creating the Page Object we wrote the object keyword, but here we need to write class. The reason is that all the tested screens so far have been in a single instance, and here we will have several list elements, each of which will be a Page Object, so we create a class, and for each element we will receive an instance of this class.

+
+

Info

+

You can read more about classes and objects in the official Kotlin documentation.

+
+

In the notes, we will need the root note_container and two TextView. If we try to find them on the screen by id, then an error will occur, since there are several such elements on the screen and it is not clear which one we need.

+

This problem is solved as follows - each note is a separate View instance and we will search for elements not on the entire screen, but only inside these same View (notes). To implement such logic, the matcher object must be passed as a parameter to the KRecyclerViewItem constructor. During testing, a matcher will be passed for each object, in which we will find the necessary View elements.

+

Therefore, we pass matcher as a parameter:

+

package com.kaspersky.kaspresso.tutorial.screen
+
+import android.view.View
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.recycler.KRecyclerItem
+import io.github.kakaocup.kakao.recycler.KRecyclerView
+import org.hamcrest.Matcher
+
+object NoteListScreen : KScreen<NoteListScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val rvNotes = KRecyclerView { withId(R.id.rv_notes) }
+
+    class NoteItemScreen(matcher: Matcher<View>) : KRecyclerItem<NoteItemScreen>(matcher) {
+
+    }
+}
+
+We can add interface elements to NoteItemScreen that we will test.

+

package com.kaspersky.kaspresso.tutorial.screen
+
+import android.view.View
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.common.views.KView
+import io.github.kakaocup.kakao.recycler.KRecyclerItem
+import io.github.kakaocup.kakao.recycler.KRecyclerView
+import io.github.kakaocup.kakao.text.KTextView
+import org.hamcrest.Matcher
+
+object NoteListScreen : KScreen<NoteListScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val rvNotes = KRecyclerView { withId(R.id.rv_notes) }
+
+    class NoteItemScreen(matcher: Matcher<View>) : KRecyclerItem<NoteItemScreen>(matcher) {
+
+        val noteContainer = KView(matcher) { withId(R.id.note_container) }
+        val tvNoteId = KTextView(matcher) { withId(R.id.tv_note_id) }
+        val tvNoteText = KTextView(matcher) { withId(R.id.tv_note_text) }
+    }
+}
+
+Pay attention to two important points:

+

First, it is now necessary to pass a matcher to the View-element constructor, in which we will search for the required object. If this is not done, the test will fail.

+

Secondly, if we check some specific behavior of the UI element, then we specify a specific inheritor of KView (KTextView, KEditText, KButton...). For example, if we want to check for text, we create a KTextView that has the ability to get the text.

+

And if we are checking some common things that are available in all interface elements (background color, size, visibility, etc.), then we can use the parent KView. In this case, we will check the texts of tvNoteId and tvNoteText, so we specified the type KTextView. And the container in which these TextView are located is an instance of CardView, we will only check the background color for it, it does not need to check any specific things, so we specified the parent type as KView

+

When the PageObject of the list item is ready, you can create an instance of KRecyclerView, for this we pass two parameters:

+

The first is builder, in which we will find RecyclerView by its id:

+

val rvNotes = KRecyclerView(
+    builder = { withId(R.id.rv_notes) },
+)
+
+The second is itemTypeBuilder, here you need to call the itemType function and to create an instance of NoteItemScreen here:

+
val rvNotes = KRecyclerView(
+    builder = { withId(R.id.rv_notes) },
+    itemTypeBuilder = {
+        itemType {
+            NoteItemScreen(it)
+        }
+    }
+)
+
+
+

Info

+

You can read more about lambda expressions here.

+
+

This entry can be shortened using Method Reference, then the final version of the class will look like this:

+

package com.kaspersky.kaspresso.tutorial.screen
+
+import android.view.View
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.common.views.KView
+import io.github.kakaocup.kakao.recycler.KRecyclerItem
+import io.github.kakaocup.kakao.recycler.KRecyclerView
+import io.github.kakaocup.kakao.text.KTextView
+import org.hamcrest.Matcher
+
+object NoteListScreen : KScreen<NoteListScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val rvNotes = KRecyclerView(
+        builder = { withId(R.id.rv_notes) },
+        itemTypeBuilder = { itemType(::NoteItemScreen) }
+    )
+
+    class NoteItemScreen(matcher: Matcher<View>) : KRecyclerItem<NoteItemScreen>(matcher) {
+
+        val noteContainer = KView(matcher) { withId(R.id.note_container) }
+        val tvNoteId = KTextView(matcher) { withId(R.id.tv_note_id) }
+        val tvNoteText = KTextView(matcher) { withId(R.id.tv_note_text) }
+    }
+}
+
+Now let's add a button to go to this screen in Page Object Main Screen

+

package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.text.KButton
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
+    val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }
+    val loginActivityButton = KButton { withId(R.id.login_activity_btn) }
+    val notificationActivityButton = KButton { withId(R.id.notification_activity_btn) }
+    val makeCallActivityButton = KButton { withId(R.id.make_call_activity_btn) }
+    val flakyActivityButton = KButton { withId(R.id.flaky_activity_btn) }
+    val listActivityButton = KButton { withId(R.id.list_activity_btn) }
+}
+
+Now you can start checking the screen with a list of notes

+

Testing NoteListScreen

+

We create a class for testing, and, as usual, add a transition to this screen:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class NoteListTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkNotesScreen() = run {
+        step("Open note list screen") {
+            MainScreen {
+                listActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+    }
+}
+
+

Now let's check that three items are displayed on the screen with the list of notes, for this we can call the getSize method on KRecyclerView:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.NoteListScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class NoteListTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkNotesScreen() = run {
+        step("Open note list screen") {
+            MainScreen {
+                listActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check notes count") {
+            NoteListScreen {
+                Assert.assertEquals(3, rvNotes.getSize())
+            }
+        }
+    }
+}
+
+

KRecyclerView has many useful methods, you can put a dot after the object name and see all the possibilities. For example, using firstChild or lastChild you can get the first or last element of NoteItemScreen respectively. You can also find an element by its position, or perform checks on absolutely all notes using the children method. To use them in angle brackets, you need to specify the type KRecyclerViewItem, in our case it is NoteItemScreen.

+

Let's check the visibility of all elements and that they all contain some text:

+

package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.NoteListScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class NoteListTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkNotesScreen() = run {
+        step("Open note list screen") {
+            MainScreen {
+                listActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check notes count") {
+            NoteListScreen {
+                Assert.assertEquals(3, rvNotes.getSize())
+            }
+        }
+        step("Check elements visibility") {
+            NoteListScreen {
+                rvNotes {
+                    children<NoteListScreen.NoteItemScreen> {
+                        tvNoteId.isVisible()
+                        tvNoteText.isVisible()
+                        noteContainer.isVisible()
+
+                        tvNoteId.hasAnyText()
+                        tvNoteText.hasAnyText()
+                    }
+                }
+            }
+        }
+    }
+}
+
+We can also test each element separately. Let's check that each note contains the correct texts and background colors:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.NoteListScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class NoteListTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkNotesScreen() = run {
+        step("Open note list screen") {
+            MainScreen {
+                listActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check notes count") {
+            NoteListScreen {
+                Assert.assertEquals(3, rvNotes.getSize())
+            }
+        }
+        step("Check elements visibility") {
+            NoteListScreen {
+                rvNotes {
+                    children<NoteListScreen.NoteItemScreen> {
+                        tvNoteId.isVisible()
+                        tvNoteText.isVisible()
+                        noteContainer.isVisible()
+
+                        tvNoteId.hasAnyText()
+                        tvNoteText.hasAnyText()
+                    }
+                }
+            }
+        }
+        step("Check elements content") {
+            NoteListScreen {
+                rvNotes {
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_green_light)
+                        tvNoteId.hasText("0")
+                        tvNoteText.hasText("Note number 0")
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(1) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_orange_light)
+                        tvNoteId.hasText("1")
+                        tvNoteText.hasText("Note number 1")
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(2) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_red_light)
+                        tvNoteId.hasText("2")
+                        tvNoteText.hasText("Note number 2")
+                    }
+                }
+            }
+        }
+    }
+}
+
+

Swipe check

+

The application has the ability to delete notes with a swipe action. Let's check this point - remove the first note and make sure that two elements with the corresponding content remain on the screen.

+

To perform some actions with View elements, we can get the view object and call its perform method as a parameter, passing the desired action. In this case, we swipe to the left, then the code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.espresso.action.ViewActions
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.NoteListScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class NoteListTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkNotesScreen() = run {
+        step("Open note list screen") {
+            MainScreen {
+                listActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check notes count") {
+            NoteListScreen {
+                Assert.assertEquals(3, rvNotes.getSize())
+            }
+        }
+        step("Check elements visibility") {
+            NoteListScreen {
+                rvNotes {
+                    children<NoteListScreen.NoteItemScreen> {
+                        tvNoteId.isVisible()
+                        tvNoteText.isVisible()
+                        noteContainer.isVisible()
+
+                        tvNoteId.hasAnyText()
+                        tvNoteText.hasAnyText()
+                    }
+                }
+            }
+        }
+        step("Check elements content") {
+            NoteListScreen {
+                rvNotes {
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_green_light)
+                        tvNoteId.hasText("0")
+                        tvNoteText.hasText("Note number 0")
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(1) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_orange_light)
+                        tvNoteId.hasText("1")
+                        tvNoteText.hasText("Note number 1")
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(2) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_red_light)
+                        tvNoteId.hasText("2")
+                        tvNoteText.hasText("Note number 2")
+                    }
+                }
+            }
+        }
+        step("Check swipe to dismiss action") {
+            NoteListScreen {
+                rvNotes {
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        view.perform(ViewActions.swipeLeft())
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_orange_light)
+                        tvNoteId.hasText("1")
+                        tvNoteText.hasText("Note number 1")
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(1) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_red_light)
+                        tvNoteId.hasText("2")
+                        tvNoteText.hasText("Note number 2")
+                    }
+                }
+            }
+        }
+    }
+}
+
+

In the last step, we remove the element at index 0 and check that “Note number 1” now lies at this index.

+

Wait for idle

+

You may have noticed that all checks are performed immediately after the swipe, without even waiting for the animation to complete. Now the test passes successfully, but sometimes it can lead to errors.

+

Therefore, in cases where some action is performed with animation and it takes time to complete, you can call the device.uiDevice.waitForIdle method. This method will stop the test execution until the screen enters the idle state - when no action is taking place and no animations are being performed.

+

We add this line to the test after the swipe, and check that the number of elements has become two:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.espresso.action.ViewActions
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.NoteListScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class NoteListTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkNotesScreen() = run {
+        step("Open note list screen") {
+            MainScreen {
+                listActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check notes count") {
+            NoteListScreen {
+                Assert.assertEquals(3, rvNotes.getSize())
+            }
+        }
+        step("Check elements visibility") {
+            NoteListScreen {
+                rvNotes {
+                    children<NoteListScreen.NoteItemScreen> {
+                        tvNoteId.isVisible()
+                        tvNoteText.isVisible()
+                        noteContainer.isVisible()
+
+                        tvNoteId.hasAnyText()
+                        tvNoteText.hasAnyText()
+                    }
+                }
+            }
+        }
+        step("Check elements content") {
+            NoteListScreen {
+                rvNotes {
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_green_light)
+                        tvNoteId.hasText("0")
+                        tvNoteText.hasText("Note number 0")
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(1) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_orange_light)
+                        tvNoteId.hasText("1")
+                        tvNoteText.hasText("Note number 1")
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(2) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_red_light)
+                        tvNoteId.hasText("2")
+                        tvNoteText.hasText("Note number 2")
+                    }
+                }
+            }
+        }
+        step("Check swipe to dismiss action") {
+            NoteListScreen {
+                rvNotes {
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        view.perform(ViewActions.swipeLeft())
+                        device.uiDevice.waitForIdle()
+                    }
+
+                    Assert.assertEquals(2, getSize())
+
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_orange_light)
+                        tvNoteId.hasText("1")
+                        tvNoteText.hasText("Note number 1")
+                    }
+
+                    childAt<NoteListScreen.NoteItemScreen>(1) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_red_light)
+                        tvNoteId.hasText("2")
+                        tvNoteText.hasText("Note number 2")
+                    }
+                }
+            }
+        }
+    }
+}
+
+

Extract methods to Page Object

+

There is one more point that we will consider in this lesson.

+

There are times when you need to add some behavior to the Page Object. For example, now you can swipe through the elements of the list. In the test, this is done with this line of code view.perform(ViewActions.swipeLeft()).

+

Every time we need to swipe, we will have to perform the same actions - get the view object, call the method passing the parameter. Instead, we can add the necessary functionality in the Page Object class and then use it where necessary.

+

Add a method to the NoteItemScreen class, let's call it swipeLeft:

+

class NoteItemScreen(matcher: Matcher<View>) : KRecyclerItem<NoteItemScreen>(matcher) {
+
+    val noteContainer = KView(matcher) { withId(R.id.note_container) }
+    val tvNoteId = KTextView(matcher) { withId(R.id.tv_note_id) }
+    val tvNoteText = KTextView(matcher) { withId(R.id.tv_note_text) }
+
+    fun swipeLeft() {
+        view.perform(ViewActions.swipeLeft())
+    }
+}
+
+Now, in any place where we need to make a swipe, we simply call the method we created on the NoteItemScreen object:

+

childAt<NoteListScreen.NoteItemScreen>(0) {
+    swipeLeft()
+    device.uiDevice.waitForIdle()
+}
+
+Then the whole test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.NoteListScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class NoteListTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkNotesScreen() = run {
+        step("Open note list screen") {
+            MainScreen {
+                listActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check notes count") {
+            NoteListScreen {
+                Assert.assertEquals(3, rvNotes.getSize())
+            }
+        }
+        step("Check elements visibility") {
+            NoteListScreen {
+                rvNotes {
+                    children<NoteListScreen.NoteItemScreen> {
+                        tvNoteId.isVisible()
+                        tvNoteText.isVisible()
+                        noteContainer.isVisible()
+
+                        tvNoteId.hasAnyText()
+                        tvNoteText.hasAnyText()
+                    }
+                }
+            }
+        }
+        step("Check elements content") {
+            NoteListScreen {
+                rvNotes {
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_green_light)
+                        tvNoteId.hasText("0")
+                        tvNoteText.hasText("Note number 0")
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(1) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_orange_light)
+                        tvNoteId.hasText("1")
+                        tvNoteText.hasText("Note number 1")
+                    }
+                    childAt<NoteListScreen.NoteItemScreen>(2) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_red_light)
+                        tvNoteId.hasText("2")
+                        tvNoteText.hasText("Note number 2")
+                    }
+                }
+            }
+        }
+        step("Check swipe to dismiss action") {
+            NoteListScreen {
+                rvNotes {
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        swipeLeft()
+                        device.uiDevice.waitForIdle()
+                    }
+
+                    Assert.assertEquals(2, getSize())
+
+                    childAt<NoteListScreen.NoteItemScreen>(0) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_orange_light)
+                        tvNoteId.hasText("1")
+                        tvNoteText.hasText("Note number 1")
+                    }
+
+                    childAt<NoteListScreen.NoteItemScreen>(1) {
+                        noteContainer.hasBackgroundColor(android.R.color.holo_red_light)
+                        tvNoteId.hasText("2")
+                        tvNoteText.hasText("Note number 2")
+                    }
+                }
+            }
+        }
+    }
+}
+
+
+

Info

+

Note that no business logic needs to be added to the Page Object. You can give these objects certain properties, add functionality, but you should not add complex logic. The Page Object should remain a screen model with described interface elements and functions for interacting with these elements.

+
+

Summary

+

In this tutorial, we learned how to test lists of items set in RecyclerView. We learned how to find elements, how to interact with them and check their behavior for compliance with the expected result.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Scenario/index.html b/Tutorial/Scenario/index.html new file mode 100644 index 000000000..79330d2a2 --- /dev/null +++ b/Tutorial/Scenario/index.html @@ -0,0 +1,2094 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 7. Reduce duplicate steps the Scenario - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Reduce duplicate steps with Scenario

+

In this lesson, we will learn what scenarios are (the Scenario class from the Kaspresso library), find out what their purpose is, when they should be used, and when it is better to avoid them.

+

Open the tutorial application and click on the Login Activity button.

+

Main Screen login button

+

We have an authorization screen where the user can enter a login and password and click on the Login button

+

Login activity

+

If the username field contains less than three characters or the password field contains less than six characters, then nothing will happen when the LOGIN button is clicked.

+

If the data is filled in correctly, then the authorization is successful and the AfterLoginActivity screen opens.

+

Screen After Login

+

It turns out that in order to check the AfterLoginActivity screen, the user must be authorized in the application. Therefore, let's first test the authorization screen LoginActivity.

+

Test LoginActivity

+

To check LoginActivity it is necessary to declare one more button inside the PageObject of the main screen - a button to go to the authorization screen.

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.text.KButton
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
+    val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }
+    val loginActivityButton = KButton { withId(R.id.login_activity_btn) }
+}
+
+

Now create a PageObject for LoginActivity, let's call it LoginScreen:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.edit.KEditText
+import io.github.kakaocup.kakao.text.KButton
+
+object LoginScreen : KScreen<LoginScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val inputUsername = KEditText { withId(R.id.input_username) }
+    val inputPassword = KEditText { withId(R.id.input_password) }
+    val loginButton = KButton { withId(R.id.login_btn) }
+}
+
+

We can create a LoginActivityTest test. Let's add a step: opening the target screen LoginActivity.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Test
+
+class LoginActivityTest : TestCase() {
+
+    @Test
+    fun test() {
+        run {
+            step("Open login screen") {
+                MainScreen {
+                    loginActivityButton {
+                        isVisible()
+                        isClickable()
+                        click()
+                    }
+                }
+            }
+        }
+    }
+}
+
+

When the target screen is open, we can test it. At the current stage, we will only add a check for a positive scenario when the user has successfully entered a login and password:

+
    +
  1. All elements are visible and the button is clickable
  2. +
  3. Input fields contain appropriate hints
  4. +
  5. If the input fields contain valid data, then transition to the next screen is performed
  6. +
+ +

In order to check which activity is currently open, you can use the method: device.activities.isCurrent(LoginActivity::class.java).

+

Then the general code of the test class will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.afterlogin.AfterLoginActivity
+import com.kaspersky.kaspresso.tutorial.login.LoginActivity
+import com.kaspersky.kaspresso.tutorial.screen.LoginScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        run {
+            val username = "123456"
+            val password = "123456"
+
+            step("Open login screen") {
+                MainScreen {
+                    loginActivityButton {
+                        isVisible()
+                        isClickable()
+                        click()
+                    }
+                }
+            }
+            step("Check elements visibility") {
+                LoginScreen {
+                    inputUsername {
+                        isVisible()
+                        hasHint(R.string.login_activity_hint_username)
+                    }
+                    inputPassword {
+                        isVisible()
+                        hasHint(R.string.login_activity_hint_password)
+                    }
+                    loginButton {
+                        isVisible()
+                        isClickable()
+                    }
+                }
+            }
+            step("Try to login") {
+                LoginScreen {
+                    inputUsername {
+                        replaceText(username)
+                    }
+                    inputPassword {
+                        replaceText(password)
+                    }
+                    loginButton {
+                        click()
+                    }
+                }
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(AfterLoginActivity::class.java)
+            }
+        }
+    }
+}
+
+

Let's start the test. Test passed successfully.

+

Now let's add checks for a negative scenario when the user entered a login or password that is less than the allowed minimum length.

+

Here you need to follow the rule: each test-case has its own test method. That is, we will not test entering both an incorrect login and incorrect password in the same method, but we will create separate ones in the same LoginActivityTest class.

+
@Test
+fun loginUnsuccessfulIfUsernameIncorrect() {
+    run {
+        val username = "12"
+        val password = "123456"
+
+        step("Open login screen") {
+            MainScreen {
+                loginActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check elements visibility") {
+            LoginScreen {
+                inputUsername {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_username)
+                }
+                inputPassword {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_password)
+                }
+                loginButton {
+                    isVisible()
+                    isClickable()
+                }
+            }
+        }
+        step("Try to login") {
+            LoginScreen {
+                inputUsername {
+                    replaceText(username)
+                }
+                inputPassword {
+                    replaceText(password)
+                }
+                loginButton {
+                    click()
+                }
+            }
+        }
+        step("Check current screen") {
+            device.activities.isCurrent(LoginActivity::class.java)
+        }
+    }
+}
+
+

Then we add a test for the case when the login is correct and the password is not.

+
@Test
+fun loginUnsuccessfulIfPasswordIncorrect() {
+    run {
+        val username = "123456"
+        val password = "12345"
+
+        step("Open login screen") {
+            MainScreen {
+                loginActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check elements visibility") {
+            LoginScreen {
+                inputUsername {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_username)
+                }
+                inputPassword {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_password)
+                }
+                loginButton {
+                    isVisible()
+                    isClickable()
+                }
+            }
+        }
+        step("Try to login") {
+            LoginScreen {
+                inputUsername {
+                    replaceText(username)
+                }
+                inputPassword {
+                    replaceText(password)
+                }
+                loginButton {
+                    click()
+                }
+            }
+        }
+        step("Check current screen") {
+            device.activities.isCurrent(LoginActivity::class.java)
+        }
+    }
+}
+
+

Let's rename the first test so that it is clear by its name that we are checking for successful authorization.

+
@Test
+fun test() 
+
+

Change to:

+
@Test
+fun loginSuccessfulIfUsernameAndPasswordCorrect()
+
+

We run the tests. They all passed successfully.

+

Take a look at the code we're using in these tests. For each test we do the following:

+
    +
  1. Declare the variables `username` and `password`, assigning different values to them depending on the check we will perform
  2. +
  3. Open the login screen
  4. +
  5. Check the visibility of elements
  6. +
  7. Enter the login and password created in step (1) in the appropriate fields and click on the "Login" button
  8. +
  9. Check that we have the desired screen
  10. +
+ +

Depending on what we check in each specific test, we have different first and last steps. In the first step we assign different values to the username and password variables, in the last step we make different checks to see if the screen is LoginActivity or AfterLoginActivity.

+

At the same time, steps from the second to the fourth are exactly the same for all tests. This is one of the cases where we can use the Scenario class.

+

Create a Scenario

+

Scenarios are classes that allow you to combine several steps into one. For example, in this case, we can create an authorization script that will go through the entire process from starting the main screen to clicking on the Login button after entering the login and password.

+

In the package with all tests com.kaspersky.kaspresso.tutorial create a new class LoginScenario and inherit from the class Scenario from the package com.kaspersky.kaspresso.testcases.api.scenario

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
+
+class LoginScenario : Scenario() {
+
+}
+
+

There is an error here, because the Scenario class is abstract, and its child needs to override the steps property, in which we must list all the steps of this scenario.

+

Press the key combination ctrl + i, select the property you want to override and press OK.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
+import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
+
+class LoginScenario : Scenario() {
+    override val steps: TestContext<Unit>.() -> Unit
+        get() = TODO("Not yet implemented")
+}
+
+

Now, after specifying the type TestContext<Unit>.() -> Unit, delete the line get() = TODO("Not yet implemented"), put the = sign and open curly brackets, in which we list all the necessary steps.

+
+

Info

+

The return type of steps is a lambda expression, which is an extension function of the TestContext class. You can read more about lambda expressions and extension functions in the official Kotlin documentation .

+
+

Let's copy the steps that are repeated in each test.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
+import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
+import com.kaspersky.kaspresso.tutorial.screen.LoginScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+
+class LoginScenario : Scenario() {
+
+    override val steps: TestContext<Unit>.() -> Unit = {
+        step("Open login screen") {
+            MainScreen {
+                loginActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check elements visibility") {
+            LoginScreen {
+                inputUsername {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_username)
+                }
+                inputPassword {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_password)
+                }
+                loginButton {
+                    isVisible()
+                    isClickable()
+                }
+            }
+        }
+        step("Try to login") {
+            LoginScreen {
+                inputUsername {
+                    replaceText(username)
+                }
+                inputPassword {
+                    replaceText(password)
+                }
+                loginButton {
+                    click()
+                }
+            }
+        }
+    }
+}
+
+

Now we have an authorization script in which we open the login screen, check the visibility of all elements, enter the login and password values and click on the Login button.

+

But there is one problem: in this class there are no username and password variables that need to be entered in the input fields. We could declare them right here inside the test, as we did in the LoginActivityTest class,

+
override val steps: TestContext<Unit>.() -> Unit = {
+    val username = "123456" // You can declare variables here
+    val password = "123456"
+
+    step("Open login screen") {
+    ...
+
+

but depending on the test being run, these values should be different, so we cannot assign a value inside the test.

+

Therefore, instead of specifying the login and password directly inside the script, we can specify them as a parameter in the Scenario class inside the constructor. Then this piece of code:

+
class LoginScenario : Scenario()
+
+

changes to:

+
class LoginScenario(
+    private val username: String,
+    private val password: String
+) : Scenario()
+
+

Now, inside the test, we do not create a login and password, but use those that were passed to us as a constructor parameter:

+
step("Try to login") {
+    LoginScreen {
+        inputUsername {
+            replaceText(username)
+        }
+        inputPassword {
+            replaceText(password)
+        }
+        loginButton {
+            click()
+        }
+    }
+}
+
+

Then the general Scenario code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
+import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
+import com.kaspersky.kaspresso.tutorial.screen.LoginScreen
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+
+class LoginScenario(
+    private val username: String,
+    private val password: String
+) : Scenario() {
+
+    override val steps: TestContext<Unit>.() -> Unit = {
+        step("Open login screen") {
+            MainScreen {
+                loginActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Check elements visibility") {
+            LoginScreen {
+                inputUsername {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_username)
+                }
+                inputPassword {
+                    isVisible()
+                    hasHint(R.string.login_activity_hint_password)
+                }
+                loginButton {
+                    isVisible()
+                    isClickable()
+                }
+            }
+        }
+        step("Try to login") {
+            LoginScreen {
+                inputUsername {
+                    replaceText(username)
+                }
+                inputPassword {
+                    replaceText(password)
+                }
+                loginButton {
+                    click()
+                }
+            }
+        }
+    }
+}
+
+

Using Scenario

+

The Scenario is ready, we can use it in tests. Let's first use the Scenario in the first test method, and then we will do it in the rest the same way:

+
    +
  1. Create a step in which we try to log in with the correct data
  2. +
  3. Call the `scenario` function
  4. +
  5. Pass the LoginScenario object as a parameter to this function
  6. +
  7. Pass the correct login and password to the LoginScenario constructor
  8. +
  9. Add a step in which we check that the `AfterLoginActivity` screen opens after login
  10. +
+ +
@Test
+fun loginSuccessfulIfUsernameAndPasswordCorrect() {
+    run {
+        step("Try to login with correct username and password") {
+            scenario(
+                LoginScenario(
+                    username = "123456",
+                    password = "123456",
+                )
+            )
+        }
+        step("Check current screen") {
+            device.activities.isCurrent(AfterLoginActivity::class.java)
+        }
+    }
+}
+
+

For the rest of the tests, we modify them the same way:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.afterlogin.AfterLoginActivity
+import com.kaspersky.kaspresso.tutorial.login.LoginActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun loginSuccessfulIfUsernameAndPasswordCorrect() {
+        run {
+            step("Try to login with correct username and password") {
+                scenario(
+                    LoginScenario(
+                        username = "123456",
+                        password = "123456",
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(AfterLoginActivity::class.java)
+            }
+        }
+    }
+
+    @Test
+    fun loginUnsuccessfulIfUsernameIncorrect() {
+        run {
+            step("Try to login with incorrect username") {
+                scenario(
+                    LoginScenario(
+                        username = "12",
+                        password = "123456",
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(LoginActivity::class.java)
+            }
+        }
+    }
+
+    @Test
+    fun loginUnsuccessfulIfPasswordIncorrect() {
+        run {
+            step("Try to login with incorrect password") {
+                scenario(
+                    LoginScenario(
+                        username = "123456",
+                        password = "12345",
+                    )
+                )
+            }
+            step("Check current screen") {
+                device.activities.isCurrent(LoginActivity::class.java)
+            }
+        }
+    }
+}
+
+

We have considered one case when Scenarios are convenient to use: when the same steps are used in different tests within the framework of testing one screen. But this is not their only purpose.

+

An application can have multiple screens that can only be accessed by being logged in. In this case, for each such screen, you will have to re-describe all the authorization steps. But when using Scenario, this becomes a very simple task.

+

Now after logging in, we have the AfterLoginActivity screen. Let's write a test for this screen.

+

First of all, we create a Page Object

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.edit.KEditText
+
+object AfterLoginScreen : KScreen<AfterLoginScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val title = KEditText { withId(R.id.title) }
+}
+
+

Add a test:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Rule
+import org.junit.Test
+
+class AfterLoginActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+
+    }
+}
+
+

In order to get to this screen, we need to go through the authorization process. Without the use of Scenario, we would have to repeat all the steps: launch the main screen, click on the button, then enter the username and password and click on the button again. But now this whole process comes down to using LoginScenario:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.AfterLoginScreen
+import org.junit.Rule
+import org.junit.Test
+
+class AfterLoginActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        run {
+            step("Open AfterLogin screen") {
+                scenario(
+                    LoginScenario(
+                        username = "123456",
+                        password = "123456"
+                    )
+                )
+            }
+            step("Check title") {
+                AfterLoginScreen {
+                    title {
+                        isVisible()
+                        hasText(R.string.screen_after_login)
+                    }
+                }
+            }
+        }
+    }
+}
+
+

Thus, through the use of Scenario, the code becomes clean, understandable and reusable. And to check the screens available only to authorized users, now you do not need to take many identical steps.

+

Best practices

+

Scenario is very handy if you use it correctly.

+
    +
  • If you have to follow the same steps to run different tests, then this is the case when it is worth creating a Scenario. Examples: screens for authorization, payment for purchases, etc.
  • +
  • You shouldn't use one Scenario inside another because this code can become very confusing, making it harder to read and reuse it, and in the end you lose all benefits of Scenarios.
  • +
  • Use Scenario only when needed. You should not create them just because sometime in the future these steps may be used in other tests. If you see that the steps are repeated in different tests, then you can create a `Scenario`, if not, you should not do this. Their number in the project should be minimal.
  • +
+ +

Summary

+

In this lesson, we learned what Scenarios are, how to create them, use them, and pass parameters to their constructor. We also considered cases when their use benefits the project, and when, on the contrary, it worsens the readability of the code, increases its coupling and complicates reuse.

+


+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Screenshot_tests_1/index.html b/Tutorial/Screenshot_tests_1/index.html new file mode 100644 index 000000000..b433b733b --- /dev/null +++ b/Tutorial/Screenshot_tests_1/index.html @@ -0,0 +1,1630 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 13. Screenshot-tests. Part 1. Simple screenshot-test - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Screenshot-тесты. Часть 1. Простой screenshot тест

+

В этом уроке мы научимся писать screenshot-тесты, узнаем, зачем они нужны, и как правильно разрабатывать приложение, чтобы его можно было покрыть тестами.

+

Продвинутый уровень

+

Ранее для успешного прохождения уроков было достаточно базовых навыков программирования на Kotlin, знания Android-разработки не требовались. Однако сегодня мы начинаем углубленное изучение фреймворка Kaspresso, и для последующих тем потребуется более глубокое понимание устройства приложений, архитектурного шаблона MVVM, применения Dependency Injection и других концепций.

+

Если у вас возникают трудности с пониманием этих тем, вы все равно можете приступить к прохождению уроков, чтобы иметь представление о возможностях Kaspresso. Однако имейте в виду, что часть материала может быть непонятной на данном этапе.

+

Тестирование LoginActivity на разных локалях

+

Чтобы узнать, зачем нужны скриншот-тесты, разберем небольшой пример. Представим, что наше приложение должно быть локализовано на французский язык. Для этого в проекте были добавлены переводы в файл strings.xml в папку values-fr.

+

French resources

+

Давайте установим на устройстве французский язык

+

Install french locale

+

и запустим LoginActivityTest.

+

Tests completed successfully

+

Тест пройден успешно, значит теоретически это приложение рабочее, и его можно раскатывать на пользователей. Но давайте откроем LoginActivity вручную (французский язык должен быть установлен на устройстве) и посмотрим, как выглядит этот экран.

+

Todo instead of strings

+

Видим, что вместо корректных текстов здесь указано «TODO: add french locale». Похоже, что разработчики во время добавления строк оставили комментарий, чтобы добавить переводы в будущем, но забыли это сделать, поэтому приложение выглядит некорректно. Обнаружить эту ошибку тесты не могут, потому что они не знают, какой должен быть текст на французском языке. По этой причине приложение работает неправильно, но тесты проходят успешно.

+

Screenshot-тесты, как решение проблемы со строками

+

Возникшую проблему могут решить скриншот-тесты. Их суть заключается в том, что для всех экранов, где пользователю отображаются строки, создаются так называемые «скриншотилки» – классы, которые делают скриншоты экрана во всех необходимых состояниях и для всех поддерживаемых языков.

+

После выполнения таких тестов скриншоты складываются в определенные папки. Затем их можно посмотреть и убедиться, что для всех локалей и для всех состояний используются корректные значения.

+

Для создания screenshot-тестов можно воспользоваться уже написанными ранее тестами, внеся в них несколько изменений. В таком случае будут выполняться те же проверки, что и раньше, но также добавится сохранение скриншотов на определенных этапах. Так можно сделать, но это не считается хорошей практикой.

+

Дело в том, что screenshot-тесты предназначены для того, чтобы предоставить снимки определенного экрана во всех возможных состояниях и для всех локалей. В некоторых случаях получение всех возможных состояний экрана может занять длительное время.

+

К примеру, вам нужно узнать, как будет выглядеть экран, если пользователь только что прошел процесс регистрации. Тогда, для того чтобы получить снимок экрана, вам придется проходить регистрацию заново, причем делать это для каждой локали. Тогда один прогон теста может занять несколько минут вместо двух-трех секунд.

+

По этой причине screenshot-тесты обычно делают максимально "легковесными":

+

Во-первых, вместо того, чтобы проходить весь процесс от старта приложения до открытия нужного экрана, мы сразу будем открывать Activity или Fragment, скриншоты которого хотим получить.

+

Во-вторых, мы не будем добавлять проверки элементов или выполнять шаги, имитирующие действия пользователя, как мы делали ранее. Наши цели –

+
    +
  1. Открыть экран
  2. +
  3. Установить нужное состояние
  4. +
  5. Сделать скриншот
  6. +
  7. При необходимости изменить состояние и снова сделать скриншот
  8. +
+ +

Дальше нужно поменять локаль и повторить все перечисленные действия.

+

Подробнее про состояния (или стейты, как их часто называют), и как их правильно устанавливать, мы поговорим в следующем уроке, а сейчас напишем простой screenshot-тест, который откроет экран LoginActivity, сделает скриншот, затем сменит язык на устройстве на французский и снова сделает скриншот.

+

Простой screenshot-тест

+

Создание screenshot-теста начинается так же, как мы делали ранее – в папке тестов создаем новый класс. Классы для скриншотов обычно называются с окончанием Screenshots. Давайте все скриншот-тесты будем хранить в отдельном пакете, назовем его screenshot_tests.

+

В этом пакете создаем класс LoginActivityScreenshots

+

Creating screenshot test class

+

У тестов, которые мы сейчас будем создавать, есть особенности: во-первых, они должны запускаться для разных локалей, во-вторых, полученные скриншоты должны быть размещены в удобной структуре папок – для каждого языка своя папка. По этим причинам тестовый класс мы унаследуем от класса DocLocScreenshotTestCase, а не от TestCase, как мы это делали ранее

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+
+class LoginActivityScreenshots : DocLocScreenshotTestCase() {
+
+}
+
+

В качестве параметра конструктору нужно передать список локалей, для которых будут делаться скриншоты. В данном случае нас интересует английский и французский языки, устанавливаем их. Делается это следующим образом:

+

package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+
+class LoginActivityScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+}
+
+Порядок, в котором будут перечислены языки, не имеет значения, тест будет запущен для каждого языка поочерёдно.

+

Как мы говорили ранее, здесь мы не будем проходить весь процесс от старта приложения до открытия необходимого экрана. Вместо этого мы сразу создадим Rule, в котором укажем, что при старте теста должен быть открыт экран LoginActivity

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.login.LoginActivity
+import org.junit.Rule
+
+class LoginActivityScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoginActivity>()
+}
+
+

В этом классе мы можем использовать такие же методы, какие использовали в других тестах. Давайте создадим один step, в котором проверим только исходное состояние экрана. Назовем метод takeScreenshots()

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.login.LoginActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoginActivity>()
+
+    @Test
+    fun takeScreenshots() = run {
+        step("Take initial state screenshots") {
+
+        }
+    }
+}
+
+

Чтобы сделать скриншоты и сохранить их в правильные папки на устройстве, необходимо вызвать метод captureScreenshot. В качестве параметра методу необходимо передать название файла, это может быть любая строка – по этому имени вы сможете найти скриншот на устройстве.

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.login.LoginActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoginActivity>()
+
+    @Test
+    fun takeScreenshots() = run {
+        step("Take initial state screenshots") {
+            captureScreenshot("Initial state")
+        }
+    }
+}
+
+

Разрешение на доступ к файлам здесь давать не нужно, это реализовано «под капотом». На данном этапе мы сделали все необходимое, чтобы получить скриншоты экрана и посмотреть, как выглядит приложение на разных локалях, но желательно сделать еще одно изменение.

+

Сейчас у нас открывается нужный экран, и сразу делается скриншот, поэтому есть вероятность, что какие-то данные на экране не успеют загрузиться, и снимок будет сделан до того, как мы увидим нужные нам элементы.

+

Чтобы решить эту проблему, перед тем, как делать скриншот, мы дождемся загрузки всех необходимых элементов интерфейса. Для всех объектов LoginScreen мы сделаем проверку на isVisible. Это проверка в своей реализации использует flakySafely, поэтому даже если данные мгновенно загружены не будут, то тест будет ждать, пока условие не выполнится в течение нескольких секунд.

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.login.LoginActivity
+import com.kaspersky.kaspresso.tutorial.screen.LoginScreen
+import org.junit.Rule
+import org.junit.Test
+
+class LoginActivityScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoginActivity>()
+
+    @Test
+    fun takeScreenshots() = run {
+        step("Take initial state screenshots") {
+            LoginScreen {
+                inputUsername.isVisible()
+                inputPassword.isVisible()
+                loginButton.isVisible()
+                captureScreenshot("Initial state")
+            }
+        }
+    }
+}
+
+

Запускаем тест. Тест пройден успешно. В Device File Explorer в папке sdcard/Documents/screenshots вы сможете найти все скриншоты, при этом для каждой локали была создана своя папка, и вы сможете просмотреть, как выглядит ваше приложение на разных языках.

+

Screenshot test results

+

Initial state en

+

Initial state fr

+

Теперь, просмотрев скриншоты, можно увидеть проблему в приложении из-за отсутствия необходимых переводов строк и исправить ошибку, добавив необходимые значения в файл values-fr/strings.xml.

+
+

Info

+

Возможно, на некоторых устройствах при смене локали у вас возникнет проблема с заголовком экрана – весь контент на экране будет корректно переведен на необходимый язык, а заголовок останется прежним. Проблема связана с багом в библиотеке Google. Его уже пофиксили, как только опубликуют соответствующий релиз, внесем изменения в Kaspresso.

+
+

Итог

+

В данном уроке мы рассмотрели: зачем нужны скриншот-тесты, как их писать и где смотреть результаты выполнения тестов.

+

Тема screenshot-тестов довольно обширная, и для более комфортного освоения мы ее разбили на несколько частей. В следующем уроке мы более подробно разберем тему стейтов, как их правильно устанавливать, и что нужно учитывать при разработке приложения, чтобы его можно было покрыть тестами.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Screenshot_tests_2/index.html b/Tutorial/Screenshot_tests_2/index.html new file mode 100644 index 000000000..3b1bf5e9b --- /dev/null +++ b/Tutorial/Screenshot_tests_2/index.html @@ -0,0 +1,2297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 14. Screenshot-tests. Part 2. Working with ViewModel and setting states - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Screenshot-тесты. Часть 2. Установка стейтов и работа с ViewModel.

+

Если в вашем приложении планируется использование screenshot-тестов, то этот момент нужно учитывать не только при написании тестов, но также при разработке приложения. В сегодняшнем уроке мы поближе познакомимся с установкой стейтов, внесем изменения в код приложения, чтобы его можно было покрыть тестами, и напишем первый скриншот тест, в котором будет работа с ViewModel.

+

Предварительные знания

+

Если вы ранее не разрабатывали приложения под Android, то сегодняшний урок может быть сложным для понимания. Поэтому мы настоятельно рекомендуем перед прохождением данного урока ознакомиться со следующими темами:

+
    +
  1. Фрагменты – что это, и как с ними работать
  2. +
  3. ViewModel и шаблон проектирования MVVM
  4. +
  5. StateFlow
  6. +
  7. Библиотека Mockk
  8. +
  9. Kotlin coroutines
  10. +
+

Обзор тестируемого приложения

+

В приложении, которое мы сегодня будем покрывать тестами, имитируется загрузка данных. При клике на кнопку начинается загрузка, отображается прогресс бар, после окончания загрузки, в зависимости от результата, мы увидим либо загруженные данные, либо сообщение об ошибке.

+

Откройте приложение tutorial и кликнете по кнопке «Load User Activity»

+

Tutorial app

+

Давайте сразу начнем разбираться, что такое стейты. После перехода по кнопке у вас откроется экран, на котором располагается одна кнопка и больше ничего нет.

+

Initial state

+

При клике на данную кнопку внешний вид экрана изменится, то есть изменится его состояние или, как его часто называют, стейт. Сейчас мы видим исходный стейт экрана, назовем его Initial.

+

Кликнете по этой кнопке и обратите внимание, как изменится стейт экрана.

+

Progress state

+

Кнопка стала неактивной, то есть по ней больше нельзя кликать, и на экране появился Progress Bar, который показывает, что идет процесс загрузки. Этот стейт отличается от исходного, назовем этот стейт Progress.

+

Через несколько секунд загрузка будет завершена, и вы увидите на экране загруженные данные пользователя (его имя и фамилию).

+

Content state

+

Это третий стейт экрана. В таком состоянии кнопка снова становится активной, прогресс бар скрывается, и на экране отображаются имя и фамилия пользователя. Назовем такой стейт Content.

+

В данном случае мы имитируем загрузку данных, в реальных приложениях работа с интернетом может завершиться с ошибкой. Такие ошибки нужно каким-то образом обрабатывать, к примеру, показывать уведомление пользователю. В нашем тестовом приложении мы сымитировали подобное поведение. Если вы попытаетесь загрузить пользователя несколько раз, то какая-то из этих попыток завершится с ошибкой и вы увидите следующее состояние экрана:

+

Error state

+

Это четвертый и последний стейт экрана, который показывает состояние ошибки, назовем его Error.

+

Простой Screenshot-тест

+

Перед тем, как мы изучим правильный способ покрытия тестами данного экрана, давайте напишем тесты тем способом, который мы уже знаем. Это нужно для того, чтобы вы понимали, почему так делать не стоит, и зачем нам вносить изменения в уже написанный код.

+

В пакете screenshot_tests создаем класс LoadUserScreenshots

+

Create class

+

Наследуемся от DocLocScreenshotTestCase и передаем список языков в качестве параметра конструктору, сделаем скриншоты для английской и французской локалей

+

package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+}
+
+Как мы говорили ранее – screenshot-тесты должны быть максимально легковесными, чтобы их прохождение занимало как можно меньше времени, поэтому вместо открытия главного экрана и перехода на экран загрузки данных пользователя, мы сразу будем открывать LoadUserActivity, создаем соответствующее правило.

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import org.junit.Rule
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+}
+
+

Для того чтобы получить все состояния экрана мы будем, как и раньше, имитировать действия пользователя – кликать по кнопке и ждать получения результата. Создаем PageObject этого экрана. В пакете com.kaspersky.kaspresso.tutorial.screen добавляем класс LoadUserScreen, тип Object

+

Create page object

+

Наследумся от KScreen и добавляем все необходимые UI-элементы: кнопка загрузки, ProgressBar, TextView с именем пользователя и TextView с текстом ошибки

+

package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.progress.KProgressBar
+import io.github.kakaocup.kakao.text.KButton
+import io.github.kakaocup.kakao.text.KTextView
+
+object LoadUserScreen : KScreen<LoadUserScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val loadingButton = KButton { withId(R.id.loading_button) }
+    val progressBarLoading = KProgressBar { withId(R.id.progress_bar_loading) }
+    val username = KTextView { withId(R.id.username) }
+    val error = KTextView { withId(R.id.error) }
+}
+
+Можем создавать скриншот-тест. Добавляем метод takeScreenshots

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+
+    @Test
+    fun takeScreenshots() {
+
+    }
+}
+
+

Первым делом давайте дождемся, пока кнопка отобразится на экране и сделаем первый скриншот исходного состояния экрана

+

package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            loadingButton.isVisible()
+            captureScreenshot("Initial state")
+        }
+    }
+}
+
+Далее необходимо кликнуть по кнопке и сохранить снимок экрана в состоянии загрузки

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            loadingButton.isVisible()
+            captureScreenshot("Initial state")
+            loadingButton.click()
+            progressBarLoading.isVisible()
+            captureScreenshot("Progress state")
+        }
+    }
+}
+
+

На этом этапе уже можно видеть одну проблему - мы загружаем данные из сети, в зависимости от скорости интернета и от работоспособности сервера скорость загрузки может меняться. Есть большая вероятность, что данные о пользователе будут загружены почти мгновенно, и состояние загрузки вы вообще не увидите, оно не будет сохранено на скриншотах, и тест упадет, потому что проверка на progressBarLoading.isVisible никогда не вернет значение true. Решение этой проблемы мы разберем далее в этом уроке.

+

Следующий этап – отображение данных о пользователе (стейт Content)

+

package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            loadingButton.isVisible()
+            captureScreenshot("Initial state")
+            loadingButton.click()
+            progressBarLoading.isVisible()
+            captureScreenshot("Progress state")
+            username.isVisible()
+            captureScreenshot("Content state")
+        }
+    }
+}
+
+Теперь нам нужно получить состояние ошибки. Как вариант, мы можем отключить интернет на устройстве, затем кликнуть по кнопке и увидеть сообщение об ошибке. Тогда нужно не забывать перед проведением тестирования, а также после него включать интернет, чтобы наш тест всегда запускался в необходимых условиях, а также не влиял на другие тесты. После включения и выключения теста добавим небольшой таймаут, чтобы быть уверенным, что интернет-подключение перешло в нужное нам состояние.

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            device.network.enable()
+            Thread.sleep(5000)
+            loadingButton.isVisible()
+            captureScreenshot("Initial state")
+            loadingButton.click()
+            progressBarLoading.isVisible()
+            captureScreenshot("Progress state")
+            username.isVisible()
+            captureScreenshot("Content state")
+            device.network.disable()
+            Thread.sleep(3000)
+            loadingButton.click()
+            error.isVisible()
+            captureScreenshot("Error state")
+            device.network.enable()
+            Thread.sleep(5000)
+        }
+    }
+}
+
+

Проблемы текущего подхода

+

Таким образом, мы смогли написать скриншот тест, в котором получили все необходимые состояния экрана, имитируя действия пользователя – кликая по кнопке и ожидая результата выполнения запроса. Но давайте подумаем, насколько эта реализация подойдет для реальных приложений.

+
    +
  1. +

    Состояние прогресса зависит от скорости интернет-соединения и от работоспособности сервера. Если ответ вернется очень быстро, то состояние прогресса мы никогда не получим, и наш тест завершится неудачно.

    +
  2. +
  3. +

    Для получения состояния ошибки мы отключаем интернет на устройстве, но в реальных приложениях часто бывает ситуация, когда в зависимости от типа ошибки поведение приложения должно отличаться. Например, если на устройстве нет интернета, то нужно показать сообщение пользователю, чтобы он включил интернет, а если сервер вернул какую-то ошибку, то отобразить соответствующий диалог. В этом случае сымитировать состояние ошибки в приложении становится очень сложной задачей.

    +
  4. +
  5. +

    Цель «скриншотилки» - сделать снимки всех возможных состояний экрана, а не проверить работоспособность приложения. Тем не менее, если в момент проведения теста будут какие-то проблемы с интернетом, или сервер не будет отвечать на запросы, то при такой реализации тест завершится неудачно, и никаких снимков мы не получим.

    +
  6. +
+

Так мы приходим к следующему выводу: получить все необходимые стейты, имитируя действия пользователя, для скриншот-тестов нецелесообразно.

+

Во-первых, некоторые состояние экрана получить очень сложно или даже невозможно.

+

Во-вторых, такие тесты начинают зависеть от многих факторов, таких как скорость интернета и работоспособность сервера, следовательно вероятность того, что эти тесты завершатся неудачно, возрастает.

+

Взаимодействие View и ViewModel

+

По причинам, рассмотренным выше, в скриншот-тестах стейты устанавливают другим способом. Имитировать действия пользователя мы не будем.

+

На этом этапе важно понимать паттерн MVVM (Model-View-ViewModel). Если говорить кратко, то согласно этому паттерну в приложении логика отделяется от видимой части.

+

Видимая часть, или можно сказать экраны (Activity и Fragments) отвечают за отображение элементов интерфейса и взаимодействия с пользователем. То есть они показывают вам какие-то элементы (кнопки, поля ввода и т.д.) и реагируют на действия пользователя (клики, свайпы и т.д). В паттерне MVVM эта часть называется View.

+

ViewModel в этом паттерне отвечает за логику.

+

Их взаимодействие выглядит следующим образом: ViewModel у себя хранит стейт экрана, она определяет, что следует показать пользователю. View получает этот стейт из ViewModel, и, в зависимости от полученного значения, отрисовывает нужные элементы. Если пользователь выполняет какие-то действия, то View вызывает соответствующий метод из ViewModel.

+

Давайте посмотрим пример из нашего приложения. На экране есть кнопка загрузки, пользователь кликнул по ней, View вызывает метод загрузки данных из ViewModel.

+

Откройте класс LoadUserFragment из пакета com.kaspersky.kaspresso.tutorial.user. Этот фрагмент в паттерне MVVM представляет собой View. В следующем фрагменте кода мы устанавливаем слушатель клика на кнопку и говорим, чтобы при клике на нее был вызван метод loadUser из ViewModel

+
binding.loadingButton.setOnClickListener {
+    viewModel.loadUser()
+}
+
+

Логика загрузки реализована внутри ViewModel. Откройте класс LoadUserViewModel из пакета com.kaspersky.kaspresso.tutorial.user.

+

При вызове этого метода ViewModel меняет стейт экрана: при старте загрузки устанавливает стейт Progress, после окончания загрузки в зависимости от результата она устанавливает стейт Error или Content.

+

fun loadUser() {
+    viewModelScope.launch {
+        _state.value = State.Progress
+        try {
+            val user = repository.loadUser()
+            _state.value = State.Content(user)
+        } catch (e: Exception) {
+            _state.value = State.Error
+        }
+    }
+}
+
+View (в данном случае фрагмент LoadUserFragment) подписывается на стейт из ViewModel и в зависимости от полученного значения меняет содержимое экрана. Происходит это в методе observeViewModel

+
private fun observeViewModel() {
+    viewLifecycleOwner.lifecycleScope.launch {
+        repeatOnLifecycle(Lifecycle.State.STARTED) {
+            viewModel.state.collect { state ->
+                when (state) {
+                    is State.Content -> {
+                        binding.progressBarLoading.isVisible = false
+                        binding.loadingButton.isEnabled = true
+                        binding.error.isVisible = false
+                        binding.username.isVisible = true
+
+                        val user = state.user
+                        binding.username.text = "${user.name} ${user.lastName}"
+                    }
+                    State.Error -> {
+                        binding.progressBarLoading.isVisible = false
+                        binding.loadingButton.isEnabled = true
+                        binding.error.isVisible = true
+                        binding.username.isVisible = false
+                    }
+                    State.Progress -> {
+                        binding.progressBarLoading.isVisible = true
+                        binding.loadingButton.isEnabled = false
+                        binding.error.isVisible = false
+                        binding.username.isVisible = false
+                    }
+                    State.Initial -> {
+                        binding.progressBarLoading.isVisible = false
+                        binding.loadingButton.isEnabled = true
+                        binding.error.isVisible = false
+                        binding.username.isVisible = false
+                    }
+                }
+            }
+        }
+    }
+}
+
+

Таким образом происходит взаимодействие View и ViewModel. ViewModel хранит стейт экрана, а View отображает его. При этом если пользователь совершил какие-то действия, то View сообщает об этом ViewModel и ViewModel меняет стейт экрана.

+

Получается, что если мы хотим изменить состояние экрана, то можно изменить значение стейта внутри ViewModel, View отреагирует на это и отрисует то, что нам нужно. Именно этим мы и будем заниматься при написании скриншот-тестов.

+

Мокирование ViewModel

+

Давайте внутри тестового класса создадим объект ViewModel, у которого будем устанавливать стейт

+

class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    val viewModel = LoadUserViewModel()
+
+    
+}
+
+Теперь в эту ViewModel внутри тестового метода мы будем устанавливать новый стейт. Давайте попробуем установить какое-то новое значение в переменную state.

+
+

Info

+

Далее мы будем работать с объектами StateFlow и MutableStateFlow, если вы не знаете, что это, и как с ними работать, обязательно прочитайте документацию

+
+

package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import com.kaspersky.kaspresso.tutorial.user.LoadUserViewModel
+import com.kaspersky.kaspresso.tutorial.user.State
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    val viewModel = LoadUserViewModel()
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            viewModel.state.value = State.Initial
+            
+        }
+    }
+}
+
+У нас возникает ошибка. Дело в том, что переменная state внутри ViewModel имеет тип StateFlow, который является неизменяемым. То есть установить новый стейт в этот объект не получится. Если вы посмотрите в код LoadUserViewModel, то увидите, что все новые значения стейта устанавливаются в переменную с нижним подчеркиванием _state, у которой тип MutableStateFlow

+

viewModelScope.launch {
+    _state.value = State.Progress
+    try {
+        val user = repository.loadUser()
+        _state.value = State.Content(user)
+    } catch (e: Exception) {
+        _state.value = State.Error
+    }
+}
+
+Эта переменная с нижним подчеркиванием является изменяемым объектом, в который можно устанавливать новые значения, но она имеет модификатор доступа private, то есть снаружи обратиться к ней не получится.

+

Как быть в этом случае? Нам необходимо добиться такого поведения, чтобы мы внутри тестового метода устанавливали новые значения стейта, а фрагмент на эти изменения реагировал. При этом фрагмент подписывается на viewModel.state без нижнего подчеркивания.

+

Можно пойти другим путем – внутри тестового класса мы создадим свой объект state, который будет изменяемый, и в который мы сможем устанавливать любые значения.

+

package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import com.kaspersky.kaspresso.tutorial.user.LoadUserViewModel
+import com.kaspersky.kaspresso.tutorial.user.State
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    val _state = MutableStateFlow<State>(State.Initial)
+    val viewModel = LoadUserViewModel()
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            _state.value = State.Initial
+            
+        }
+    }
+}
+
+Теперь нужно сделать так, чтобы в тот момент, когда фрагмент подписывается на viewModel.state вместо настоящей реализации подставлялся наш только что созданный объект. Сделать это можно при помощи библиотеки Mockk. Если вы не работали с этой библиотекой ранее, советуем почитать официальную документацию +Для использования данной библиотеки необходимо добавить зависимости в файл build.gradle

+
androidTestImplementation("io.mockk:mockk-android:1.13.3")
+
+

Gradle

+
+

Info

+

Если после синхронизации и запуска проекта у вас возникают ошибки, следуйте инструкциям в журнале ошибок. В случае, если разобраться не получилось, переключитесь на ветку TECH-tutorial-results и сверьте файл build.gradle из этой ветки с вашим

+
+

Теперь внутренняя реализация ViewModel нас не интересует. Все, что нам нужно – чтобы фрагмент подписывался на state из ViewModel, а ему возвращался тот объект, который мы создали внутри тестового класса. Делается это следующим образом:

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import com.kaspersky.kaspresso.tutorial.user.LoadUserViewModel
+import com.kaspersky.kaspresso.tutorial.user.State
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    val _state = MutableStateFlow<State>(State.Initial)
+    val viewModel = mockk<LoadUserViewModel>(relaxed = true){
+        every { state } returns _state
+    }
+
+    
+}
+
+

То, что мы сделали, называется мокированием. Мы «замокали» ViewModel таким образом, что если кто-то будет работать с ней и обратится к ее полю state, то ему вернется созданный нами объект _state. Настоящая реализация LoadUserViewModel в тестах использоваться не будет.

+

Теперь нам не нужно имитировать действия пользователя для получения различных состояний экрана. Вместо этого, мы будем устанавливать стейт в переменную _state и затем делать скриншот.

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import com.kaspersky.kaspresso.tutorial.user.LoadUserViewModel
+import com.kaspersky.kaspresso.tutorial.user.State
+import com.kaspersky.kaspresso.tutorial.user.User
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    val _state = MutableStateFlow<State>(State.Initial)
+    val viewModel = mockk<LoadUserViewModel>(relaxed = true) {
+        every { state } returns _state
+    }
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            _state.value = State.Initial
+            captureScreenshot("Initial state")
+            _state.value = State.Progress
+            captureScreenshot("Progress state")
+            _state.value = State.Content(user = User(name = "Test", lastName = "Test"))
+            captureScreenshot("Content state")
+            _state.value = State.Error
+            captureScreenshot("Error state")
+        }
+    }
+}
+
+

Дорабатываем код фрагмента

+

Если мы запустим тест в таком виде, то работать он будет неправильно, никакой смены состояний экрана происходить не будет. Дело в том, что мы создали объект viewModel, но нигде его не используем.

+

Давайте посмотрим, как происходит взаимодействие экрана и ViewModel, и какие изменения нужно внести в код, чтобы экран взаимодействовал не с настоящей ViewModel, а с замоканной.

+

Для открытия экрана мы запускаем LoadUserActivity

+

package com.kaspersky.kaspresso.tutorial.user
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.kaspersky.kaspresso.tutorial.R
+
+class LoadUserActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_load_user)
+        if (savedInstanceState == null) {
+            supportFragmentManager.beginTransaction()
+                .replace(R.id.fragment_container, LoadUserFragment.newInstance())
+                .commit()
+        }
+    }
+}
+
+В этой Activity почти нет кода. Дело в том, что в последнее время большинство приложений используют подход Single Activity. При таком подходе все экраны создаются на фрагментах, а активити служит лишь контейнером для них. Если вы хотите узнать больше о преимуществах этого подхода, то мы советуем почитать документацию. Что нужно понимать сейчас – внешний вид экрана и взаимодействие с ViewModel реализовано внутри LoadUserFragment, а LoadUserActivity представляет собой контейнер, в который мы этот фрагмент установили. Следовательно, изменения нужно делать именно внутри фрагмента.

+

Открываем LoadUserFragment

+

package com.kaspersky.kaspresso.tutorial.user
+
+class LoadUserFragment : Fragment() {
+
+
+
+    private lateinit var viewModel: LoadUserViewModel
+
+
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        viewModel = ViewModelProvider(this)[LoadUserViewModel::class.java]
+
+}
+
+Обратите внимание, что в этом классе есть приватная переменная viewModel, а в методе onViewCreated мы этой переменной присваиваем значение, создавая объект при помощи ViewModelProvider. Нам необходимо добиться такого поведения, чтобы при обычном использовании фрагмента вьюмодель создавалась через ViewModelProvider, а если этот фрагмент используется в screenshot-тестах, то должна быть возможность передать замоканную вьюмодель в качестве параметра.

+

Для создания экземпляра фрагмента мы используем фабричный метод newInstance

+

companion object {
+
+    fun newInstance(): LoadUserFragment = LoadUserFragment()
+}
+
+В этом методе мы просто создаем объект LoadUserFragment. Давайте добавим еще один метод, который будет принимать в качестве параметра замоканную вьюмодель и устанавливать это значение в поле фрагмента. Этот метод мы будем использовать в тестах, поэтому давайте назовем его newTestInstance

+

companion object {
+
+    fun newInstance(): LoadUserFragment = LoadUserFragment()
+
+    fun newTestInstance(
+        mockedViewModel: LoadUserViewModel
+    ): LoadUserFragment = LoadUserFragment().apply {
+        viewModel = mockedViewModel
+    }
+}
+
+Теперь для создания фрагмента в активити мы будем вызывать метод newInstance, что мы сейчас и делаем

+

if (savedInstanceState == null) {
+    supportFragmentManager.beginTransaction()
+        .replace(R.id.fragment_container, LoadUserFragment.newInstance())
+        .commit()
+}
+
+А для создания фрагмента внутри скриншот-тестов будем вызывать метод newTestInstance.

+

На данном этапе в методе onViewCreated мы присваиваем значение переменной viewModel независимо от того, используется этот фрагмент для скриншот-тестов или нет. Давайте это исправим, добавим поле isForScreenshots типа Boolean, по умолчанию установим значение false, а в методе newTestInstance установим значение true.

+

package com.kaspersky.kaspresso.tutorial.user
+
+
+
+class LoadUserFragment : Fragment() {
+
+    
+
+    private lateinit var viewModel: LoadUserViewModel
+    private var isForScreenshots = false
+
+    
+    companion object {
+
+        fun newInstance(): LoadUserFragment = LoadUserFragment()
+
+        fun newTestInstance(
+            mockedViewModel: LoadUserViewModel
+        ): LoadUserFragment = LoadUserFragment().apply {
+            viewModel = mockedViewModel
+            isForScreenshots = true
+        }
+    }
+}
+
+В методе onViewCreated мы будем создавать вьюмодель через ViewModelProvider только в том случае, если isForScreenshots равен false

+

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+    super.onViewCreated(view, savedInstanceState)
+    if (!isForScreenshots) {
+        viewModel = ViewModelProvider(this)[LoadUserViewModel::class.java]
+    }
+    binding.loadingButton.setOnClickListener {
+        viewModel.loadUser()
+    }
+    observeViewModel()
+}
+
+После создания вьюмодели мы устанавливаем слушатель клика на кнопку загрузки и в этом слушателе вызываем метод вьюмодели. В случае, если мы передали замоканный вариант ViewModel, вызов этого метода viewModel.loadUser() приведет к падению теста, так как у нее данный метод не реализован. Поэтому вызов любых методов вьюмодели мы также будем выполнять только в том случае, если этот фрагмент используется не для скриншот-тестов:

+

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+    super.onViewCreated(view, savedInstanceState)
+    if (!isForScreenshots) {
+        viewModel = ViewModelProvider(this)[LoadUserViewModel::class.java]
+        binding.loadingButton.setOnClickListener {
+            viewModel.loadUser()
+        }
+    }
+    observeViewModel()
+}
+
+Как вы должны помнить, в тестах мы замокали значение переменной state из вьюмодели

+

val _state = MutableStateFlow<State>(State.Initial)
+val viewModel = mockk<LoadUserViewModel>(relaxed = true) {
+    every { state } returns _state
+}
+
+Поэтому, когда мы обратимся к полю viewModel.state из фрагмента в методе observeViewModel

+

viewModel.state.collect { state ->
+    when (state) {
+        is State.Content -> {
+            
+
+то ошибки не будет, вместо настоящей реализации будет использовано значение из переменной _state, созданной внутри теста.

+

Тестирование фрагментов

+

Теперь, для того чтобы написать скриншот тест, нам нужно внутри этого теста создать экземпляр фрагмента, передав ему замоканную вьюмодель в качестве параметра. Но если вы посмотрите на текущий код, то увидите, что мы вообще не создаем здесь никаких фрагментов

+

package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserActivity
+import com.kaspersky.kaspresso.tutorial.user.LoadUserViewModel
+import com.kaspersky.kaspresso.tutorial.user.State
+import com.kaspersky.kaspresso.tutorial.user.User
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Rule
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    val _state = MutableStateFlow<State>(State.Initial)
+    val viewModel = mockk<LoadUserViewModel>(relaxed = true) {
+        every { state } returns _state
+    }
+
+    @get:Rule
+    val activityRule = activityScenarioRule<LoadUserActivity>()
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            _state.value = State.Initial
+            captureScreenshot("Initial state")
+            _state.value = State.Progress
+            captureScreenshot("Progress state")
+            _state.value = State.Content(user = User(name = "Test", lastName = "Test"))
+            captureScreenshot("Content state")
+            _state.value = State.Error
+            captureScreenshot("Error state")
+        }
+    }
+}
+
+У нас открывается LoadUserActivity, а внутри этой активити создается фрагмент, поэтому передать в тот фрагмент никакие параметры мы не можем.

+

Если мы тестируем фрагменты, то запускать активити, в которой этот фрагмент лежит, не нужно. Мы можем напрямую тестировать фрагменты. Для того чтобы у нас была такая возможность, необходимо добавить следующие зависимости в build.gradle

+

Gradle

+
debugImplementation("androidx.fragment:fragment-testing-manifest:1.6.0"){
+    isTransitive = false
+}
+androidTestImplementation("androidx.fragment:fragment-testing:1.6.0")
+
+

После синхронизации проекта открываем класс LoadUserScreenshots и удаляем из него activityRule, запускать активити нам больше не нужно.

+

Для того чтобы запустить фрагмент, необходимо вызвать метод launchFragmentInContainer и в фигурных скобках создать фрагмент, который нужно отобразить +

package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.fragment.app.testing.launchFragmentInContainer
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserFragment
+import com.kaspersky.kaspresso.tutorial.user.LoadUserViewModel
+import com.kaspersky.kaspresso.tutorial.user.State
+import com.kaspersky.kaspresso.tutorial.user.User
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    val _state = MutableStateFlow<State>(State.Initial)
+    val viewModel = mockk<LoadUserViewModel>(relaxed = true) {
+        every { state } returns _state
+    }
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            launchFragmentInContainer {
+                LoadUserFragment.newTestInstance(mockedViewModel = viewModel)
+            }
+            _state.value = State.Initial
+            captureScreenshot("Initial state")
+            _state.value = State.Progress
+            captureScreenshot("Progress state")
+            _state.value = State.Content(user = User(name = "Test", lastName = "Test"))
+            captureScreenshot("Content state")
+            _state.value = State.Error
+            captureScreenshot("Error state")
+        }
+    }
+}
+

+

Итак, давайте обсудим, что здесь происходит. Внутри метода takeScreenshots мы запускаем фрагмент LoadUserFragment. Для создания фрагмента мы воспользовались методом newTestInstance, передавая созданный в тестовом классе вариант вьюмодели.

+

Теперь фрагмент работает не с настоящей вьюмоделью, а с замоканной. Фрагмент отрисовывает стейт, который лежит внутри вьюмодели, но так как мы подменили (замокали) объект state, то фрагмент покажет то состояние, которое мы установим в тестовом классе.

+

С этого момента нам не нужно имитировать действия пользователя, мы просто устанавливаем необходимое состояние, фрагмент его отрисовывает, и мы делаем скриншот.

+

Если вы сейчас запустите тест, то увидите, что скриншоты всех состояний успешно сохраняются в папку на устройстве, и это происходит гораздо быстрее, чем в предыдущем варианте теста.

+

Меняем стиль

+

Вы могли обратить внимание, что внешний вид экрана в приложении отличается от того, который мы получили в результате выполнения теста. Проблема заключается в использовании стилей. Во время теста под капотом создается активити, которая является контейнером для нашего фрагмента. Стиль этой активити может отличаться от того, который используется у нас в приложении.

+

Данная проблема решается очень просто – в качестве параметра в метод launchFragmentInContainer можно передать стиль, который должен использоваться внутри фрагмента, его можно найти в манифесте приложения

+

Style

+

Передать этот стиль в метод launchFragmentInContainer можно следующим образом:

+
package com.kaspersky.kaspresso.tutorial.screenshot_tests
+
+import androidx.fragment.app.testing.launchFragmentInContainer
+import com.kaspersky.kaspresso.testcases.api.testcase.DocLocScreenshotTestCase
+import com.kaspersky.kaspresso.tutorial.R
+import com.kaspersky.kaspresso.tutorial.screen.LoadUserScreen
+import com.kaspersky.kaspresso.tutorial.user.LoadUserFragment
+import com.kaspersky.kaspresso.tutorial.user.LoadUserViewModel
+import com.kaspersky.kaspresso.tutorial.user.State
+import com.kaspersky.kaspresso.tutorial.user.User
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Test
+
+class LoadUserScreenshots : DocLocScreenshotTestCase(locales = "en, fr") {
+
+    val _state = MutableStateFlow<State>(State.Initial)
+    val viewModel = mockk<LoadUserViewModel>(relaxed = true) {
+        every { state } returns _state
+    }
+
+    @Test
+    fun takeScreenshots() {
+        LoadUserScreen {
+            launchFragmentInContainer(
+                themeResId = R.style.Theme_Kaspresso
+            ) {
+                LoadUserFragment.newTestInstance(mockedViewModel = viewModel)
+            }
+            _state.value = State.Initial
+            captureScreenshot("Initial state")
+            _state.value = State.Progress
+            captureScreenshot("Progress state")
+            _state.value = State.Content(user = User(name = "Test", lastName = "Test"))
+            captureScreenshot("Content state")
+            _state.value = State.Error
+            captureScreenshot("Error state")
+        }
+    }
+}
+
+

Итог

+

Это был очень насыщенный урок, в котором мы научились правильно устанавливать стейты во вьюмодель, тестировать фрагменты, использовать бибилотеку Mockk для мокирования сущностей, а также дорабатывать код приложения, чтобы его можно было покрыть screenshot-тестами.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Steps_and_sections/index.html b/Tutorial/Steps_and_sections/index.html new file mode 100644 index 000000000..8f48d44f7 --- /dev/null +++ b/Tutorial/Steps_and_sections/index.html @@ -0,0 +1,1957 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 6. Steps and sections - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Sections and steps

+

Improve the code

+

In the last lesson, we wrote a test for the Internet availability screen, the test class code looked like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.device.exploit.Exploit
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            wifiActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+        WifiScreen {
+            device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+            checkWifiButton.isVisible()
+            checkWifiButton.isClickable()
+            wifiStatus.hasEmptyText()
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.enabled_status)
+            device.network.toggleWiFi(false)
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.disabled_status)
+            device.exploit.rotate()
+            wifiStatus.hasText(R.string.disabled_status)
+        }
+    }
+}
+
+

And we talked about how one of the problems with this code is that it is difficult to read and maintain even at this stage, and if the functionality of the screen expands and we have to add more tests, then the code will become completely unreadable.

+

In fact, usually any tests (including manual ones) are performed on test cases. That is, the tester has a sequence of steps that he performs to check the performance of the screen. In our case, we have this sequence of steps, but it is written in one block of code and it is not clear where one step ends and another begins. We can solve this problem with comments.

+

Let's copy this WifiSampleTest class and paste it into the same package, but with a different name WifiSampleWithStepsTest. This is necessary so that you can then compare the new and old implementations of this test. We will not change the WifiSampleTest code today. Now in the new class WifiSampleWithStepsTest we add comments to each step.

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.device.exploit.Exploit
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleWithStepsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        // Step 1. Open target screen
+        MainScreen {
+            wifiActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+        WifiScreen {
+            // Step 2. Check correct wifi status
+            device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+            checkWifiButton.isVisible()
+            checkWifiButton.isClickable()
+            wifiStatus.hasEmptyText()
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.enabled_status)
+            device.network.toggleWiFi(false)
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.disabled_status)
+
+            // Step 3. Rotate device and check wifi status
+            device.exploit.rotate()
+            wifiStatus.hasText(R.string.disabled_status)
+        }
+    }
+}
+
+

This slightly improved the readability of the code, but did not solve all the problems. For example, if your test fails, how do you know at what step it happened? You will have to examine the logs, trying to figure out what went wrong. It would be much better if the logs showed entries like Step 1 started -> ... -> Step 1 succeed or Step 2 started -> ... -> Step 2 failed. This will allow you to immediately determine by the notes in the log at what stage the problem arose.

+

To do this, we can manually add output to the log for each step before and after its execution and wrap it all in a try catch block to make the test failure also recorded in logs. In this case, our test would look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import android.util.Log
+import androidx.test.core.app.takeScreenshot
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.device.exploit.Exploit
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleWithStepsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        try {
+            Log.i("KASPRESSO", "Step 1. Open target screen -> started")
+            MainScreen {
+                wifiActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+            Log.i("KASPRESSO", "Step 1. Open target screen -> succeed")
+        } catch (e: Throwable) {
+            Log.i("KASPRESSO", "Step 1. Open target screen -> failed")
+            takeScreenshot()
+        }
+        WifiScreen {
+            try {
+                Log.i("KASPRESSO", "Step 2. Check correct wifi status -> started")
+                device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+                checkWifiButton.isVisible()
+                checkWifiButton.isClickable()
+                wifiStatus.hasEmptyText()
+                checkWifiButton.click()
+                wifiStatus.hasText(R.string.enabled_status)
+                device.network.toggleWiFi(false)
+                checkWifiButton.click()
+                wifiStatus.hasText(R.string.disabled_status)
+                Log.i("KASPRESSO", "Step 2. Check correct wifi status -> succeed")
+            } catch (e: Throwable) {
+                Log.i("KASPRESSO", "Step 2. Check correct wifi status -> failed")
+            }
+
+            try {
+                Log.i("KASPRESSO", "Step 3. Rotate device and check wifi status -> started")
+                device.exploit.rotate()
+                wifiStatus.hasText(R.string.disabled_status)
+                Log.i("KASPRESSO", "Step 3. Rotate device and check wifi status -> succeed")
+            } catch (e: Throwable) {
+                Log.i("KASPRESSO", "Step 3. Rotate device and check wifi status -> failed")
+                takeScreenshot()
+            }
+        }
+    }
+}
+
+

Let's turn on the Internet on the device and check the operation of our test.

+

Let's launch the test. It passed successfully.

+

Now let's see the logs. To do this, open the Logcat tab at the bottom of Android Studio

+

Logcat

+

There are a lot of logs displayed here and finding ours is quite difficult. We can filter the logs by the tag we specified ("KASPRESSO"). To do this, click on the arrow at the top right of Logcat window and select Edit Configuration

+

Edit configuration

+

A filter creation window will open. Add the name of the filter and the tag that we are interested in:

+

Create filter

+

Now we can see only useful information. Let's clear the log

+

Clear logcat

+

and run the test again. Do not forget to turn on the Internet on the device before this. Read the logs:

+

Log step 1

+

Here are the logs we added: step 1 is run, then checks are done, then step 1 succeeds.

+

Looking further:

+

Log step 2

+

Log step 2

+

With the second and third steps, everything is also fine. We understand when and what step starts the execution, we can see the specific actions that the test is currently performing, and we can see the result of the test.

+

Now let's turn off the Internet and run the test again. According to our logic, the test should fail.

+

Even though the test should have failed, all tests are green. We look at the log - now we are interested in step 2, which should have failed due to the fact that the Internet was initially turned off on the device.

+

Log step 2 failed

+

Judging by the logs, step 2 really failed. The status of the header was checked, the text did not match, the program made several more attempts to check that the text on the header contains the text enabled, but all these attempts were unsuccessful and the step ended with an error. Why do we have green tests in this case?

+

The fact is that if the test fails, then an exception is thrown, and if no one handled this exception in the try catch block, then the tests will be red. But we handle all exceptions in the code in order to make an entry in the log that the test ended with an error.

+
try {
+        ...
+} catch (e: Throwable) {
+    /**
+     * Мы обработали исключение и дальше оно проброшено не будет, поэтому такой 
+     * тест считается выполненным успешно
+     */
+    Log.i("KASPRESSO", "Step 2. Check correct wifi status -> failed")
+}
+
+

To solve this problem, it is necessary to throw this exception further after the error message is output to the log so that the test fails. This is done using the throw keyword. Then the test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import android.util.Log
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.device.exploit.Exploit
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleWithStepsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        try {
+            Log.i("KASPRESSO", "Step 1. Open target screen -> started")
+            MainScreen {
+                wifiActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+            Log.i("KASPRESSO", "Step 1. Open target screen -> succeed")
+        } catch (e: Throwable) {
+            Log.i("KASPRESSO", "Step 1. Open target screen -> failed")
+            throw e
+        }
+        WifiScreen {
+            try {
+                Log.i("KASPRESSO", "Step 2. Check correct wifi status -> started")
+                device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+                checkWifiButton.isVisible()
+                checkWifiButton.isClickable()
+                wifiStatus.hasEmptyText()
+                checkWifiButton.click()
+                wifiStatus.hasText(R.string.enabled_status)
+                device.network.toggleWiFi(false)
+                checkWifiButton.click()
+                wifiStatus.hasText(R.string.disabled_status)
+                Log.i("KASPRESSO", "Step 2. Check correct wifi status -> succeed")
+            } catch (e: Throwable) {
+                Log.i("KASPRESSO", "Step 2. Check correct wifi status -> failed")
+                throw e
+            }
+
+            try {
+                Log.i("KASPRESSO", "Step 3. Rotate device and check wifi status -> started")
+                device.exploit.rotate()
+                wifiStatus.hasText(R.string.disabled_status)
+                Log.i("KASPRESSO", "Step 3. Rotate device and check wifi status -> succeed")
+            } catch (e: Throwable) {
+                Log.i("KASPRESSO", "Step 3. Rotate device and check wifi status -> failed")
+                throw e
+            }
+        }
+    }
+}
+
+

Let's run the test again. Now it ends with an error and we have understandable logs, where you can immediately see at which step the error occurred. After step 2 there is nothing else in the logs.

+

The code that we wrote is working, but very cumbersome, and we have to write a whole canvas of the same code for each step (logs, try catch blocks, etc.).

+

Steps

+

In order to simplify writing tests and make the code more readable and extendable, steps have been added to Kaspresso. They implement everything that we just wrote by hand "under the hood".

+

To use steps, you need to call the run {} method and list all the steps that will be performed during the test in curly brackets. Each step must be called inside the step function.

+

Let's write it in code. First, we remove all unnecessary logs and try catch blocks.

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.device.exploit.Exploit
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleWithStepsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            wifiActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+
+        WifiScreen {
+            device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+            checkWifiButton.isVisible()
+            checkWifiButton.isClickable()
+            wifiStatus.hasEmptyText()
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.enabled_status)
+            device.network.toggleWiFi(false)
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.disabled_status)
+
+            device.exploit.rotate()
+            wifiStatus.hasText(R.string.disabled_status)
+        }
+    }
+}
+
+

Now, at the beginning of the test, we call the run method, inside which we call the step function for each step. We pass the name of the step as a parameter to this function.

+
@Test
+    fun test() {
+        run {
+            step("Open target screen") {
+                ...
+            }
+            step("Check correct wifi status") {
+                ...
+            }
+            step("Rotate device and check wifi status") {
+                ...
+            }
+        }
+    }
+
+

Within each step, we specify the actions that are required for that step. The actions stay the same as before. Then the test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.device.exploit.Exploit
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleWithStepsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        run {
+            step("Open target screen") {
+                MainScreen {
+                    wifiActivityButton {
+                        isVisible()
+                        isClickable()
+                        click()
+                    }
+                }
+            }
+            step("Check correct wifi status") {
+                WifiScreen {
+                    device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+                    checkWifiButton.isVisible()
+                    checkWifiButton.isClickable()
+                    wifiStatus.hasEmptyText()
+                    checkWifiButton.click()
+                    wifiStatus.hasText(R.string.enabled_status)
+                    device.network.toggleWiFi(false)
+                    checkWifiButton.click()
+                    wifiStatus.hasText(R.string.disabled_status)
+                }
+            }
+            step("Rotate device and check wifi status") {
+                WifiScreen {
+                    device.exploit.rotate()
+                    wifiStatus.hasText(R.string.disabled_status)
+                }
+            }
+        }
+    }
+}
+
+

Turn on the Internet on the device and run the test. Test passed successfully. Let's look at the logs:

+

Log with steps

+

Thus, thanks to the use of steps, not only our code has become more understandable and easy to understand, but also the logs have a clear structure and allow you to quickly determine which steps were performed and what the result of these operations is.

+

Let's run this test again now with the internet off. The test falls. Let's look at the logs.

+

Test fail with steps

+

Now it becomes much easier to find an error in the test, thanks to understandable logs.

+

Before and After sections

+

Our code has become much better, but one important problem remains. It is necessary to reset the device to a default state before each test: the Internet must be turned on and the portrait orientation must be set.

+

Kaspresso has the ability to add before and after blocks. The code inside the before block will be executed before the test, and this is where we can set the defaults. The code inside the after block will be executed after the test. During the test, the state of the phone may change: we can turn off the Internet, change orientation, but after the test we need to return to the original state. We will do this inside the after block.

+

Then the test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.device.exploit.Exploit
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleWithStepsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        before {
+            /**
+             * Set portrait orientation and enable Wifi before the test
+             */
+            device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+            device.network.toggleWiFi(true)
+        }.after {
+            /**
+             * Reset the default state after the test
+             */
+            device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+            device.network.toggleWiFi(true)
+        }.run {
+            step("Open target screen") {
+                MainScreen {
+                    wifiActivityButton {
+                        isVisible()
+                        isClickable()
+                        click()
+                    }
+                }
+            }
+            step("Check correct wifi status") {
+                WifiScreen {
+                    checkWifiButton.isVisible()
+                    checkWifiButton.isClickable()
+                    wifiStatus.hasEmptyText()
+                    checkWifiButton.click()
+                    wifiStatus.hasText(R.string.enabled_status)
+                    device.network.toggleWiFi(false)
+                    checkWifiButton.click()
+                    wifiStatus.hasText(R.string.disabled_status)
+                }
+            }
+            step("Rotate device and check wifi status") {
+                WifiScreen {
+                    device.exploit.rotate()
+                    wifiStatus.hasText(R.string.disabled_status)
+                }
+            }
+        }
+    }
+}
+
+

The test is almost ready, we can add one small improvement. Now after flipping the device, we check that the text is still the same, but we don't check that the orientation has actually changed. If it turns out that if the device.expoit.rotate() method did not work for some reason, then the orientation will not change and the check for text will be useless. Let's add a check that the device's orientation is landscape.

+

Assert.assertTrue(device.context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)

+

Now the complete test code looks like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import android.content.res.Configuration
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.device.exploit.Exploit
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleWithStepsTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        before {
+            device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+            device.network.toggleWiFi(true)
+        }.after {
+            device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+            device.network.toggleWiFi(true)
+        }.run {
+            step("Open target screen") {
+                MainScreen {
+                    wifiActivityButton {
+                        isVisible()
+                        isClickable()
+                        click()
+                    }
+                }
+            }
+            step("Check correct wifi status") {
+                WifiScreen {
+                    checkWifiButton.isVisible()
+                    checkWifiButton.isClickable()
+                    wifiStatus.hasEmptyText()
+                    checkWifiButton.click()
+                    wifiStatus.hasText(R.string.enabled_status)
+                    device.network.toggleWiFi(false)
+                    checkWifiButton.click()
+                    wifiStatus.hasText(R.string.disabled_status)
+                }
+            }
+            step("Rotate device and check wifi status") {
+                WifiScreen {
+                    device.exploit.rotate()
+                    Assert.assertTrue(device.context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
+                    wifiStatus.hasText(R.string.disabled_status)
+                }
+            }
+        }
+    }
+}
+
+

Summary

+

In this lesson, we've significantly improved our code, making it cleaner, clearer, and easier to maintain. This is made possible by Kaspresso's step, before and after functions. We also learned how to output messages to the log, as well as read the logs, filter and analyze them.

+


+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/UiAutomator/index.html b/Tutorial/UiAutomator/index.html new file mode 100644 index 000000000..9af7b8a8d --- /dev/null +++ b/Tutorial/UiAutomator/index.html @@ -0,0 +1,2037 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 8. Kautomator. Third Party Application Testing - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Kautomator. Third Party Application Testing

+

In previous lessons, we learned how to write tests for user interface elements that are located in our application. But there are often cases when this is not enough for full-fledged testing, and in addition to our application, we need to perform some actions outside of it.

+

As an example, let's check the start screen of the Google Play app in an unauthorized state.

+
    +
  1. Open Google Play
  2. +
  3. Check that there is a `Sign In` button on the screen
  4. +
+ +

Google play unauthorized

+

Do not forget to log out before starting the test.

+

Autotest for Google Play functionality

+

Let's start writing a test - create a class GooglePlayTest and inherit it from TestCase:

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+
+class GooglePlayTest : TestCase() {
+
+}
+
+

Add a test method:

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Test
+
+class GooglePlayTest : TestCase() {
+
+    @Test
+    fun testNotSignIn() = run {
+
+    }
+}
+
+

The first step we need to take is to launch the Google Play application, for this we need the name of its package. Google Play's package name is com.android.vending, later we will show where you can find this information.

+

We will use this package name in the test several times, therefore, in order not to duplicate the code, we will create a constant where we will put this name:

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Test
+
+class GooglePlayTest : TestCase() {
+
+    @Test
+    fun testNotSignIn() = run {
+
+    }
+
+    companion object {
+
+        private const val GOOGLE_PLAY_PACKAGE = "com.android.vending"
+    }
+}
+
+

To launch any screen in Android, we need an Intent object. To get the required Intent we will use the following code:

+
val intent = device.targetContext.packageManager.getLaunchIntentForPackage(GOOGLE_PLAY_PACKAGE)
+
+

Here several objects that may be unfamiliar to you are used at once: Context, PackageManager and Intent. You can read more about them in the documentation.

+

In short, Context provides access to various application resources and allows you to perform many actions, including opening screens using Intents. The Intent contains information about the screen we want to open, and the PackageManager in this case allows you to get an Intent to open the start screen of a particular application by its package name.

+
+

Info

+

To get the Context, you can use the targetContext and context methods of the device object. They have one significant difference. +When we want to check the operation of some application and run an autotest, in fact, two applications are installed on the device: the one that we are testing (in this case, the tutorial) and the second, which runs all the test scripts. +When we call the targetContext method, we refer to the application under test (tutorial), and if we call the context method, then the call will be to the second application that runs the tests.

+
+
val intent = device.targetContext.packageManager.getLaunchIntentForPackage(GOOGLE_PLAY_PACKAGE)
+
+

In the above code we first get the targetContext from the device object, like we already did in one of the previous lessons. Then, from targetContext we get packageManager, from which we can get the Intent to launch the Google Play screen using the getLaunchIntentForPackage method.

+

This method returns an Intent to launch the start screen of the application whose package was passed as a parameter. To do this, we pass the package name of the application we want to run, in this case, Google Play.

+

We got Intent, now we use it to launch the screen. To do this, call the startActivity method on the targetContext object and pass intent as a parameter:

+
val intent = device.targetContext.packageManager.getLaunchIntentForPackage(GOOGLE_PLAY_PACKAGE)
+device.targetContext.startActivity(intent)
+
+

In this code, we get the targetContext twice from the device object. In order not to duplicate code, you can shorten this entry by using the with function

+
+

Info

+

You can read more about with and other scope functions in documentation.

+
+

Then the test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Test
+
+class GooglePlayTest : TestCase() {
+
+    @Test
+    fun testNotSignIn() = run {
+        step("Open Google Play") {
+            with(device.targetContext) {
+                val intent = packageManager.getLaunchIntentForPackage(GOOGLE_PLAY_PACKAGE)
+                startActivity(intent)
+            }
+        }
+    }
+
+    companion object {
+
+        private const val GOOGLE_PLAY_PACKAGE = "com.android.vending"
+    }
+}
+
+

If you are not familiar with the with, apply, and other scope functions, you can rewrite code without them, in which case the test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Test
+
+class GooglePlayTest : TestCase() {
+
+    @Test
+    fun testNotSignIn() = run {
+        step("Open Google Play") {
+            val intent = device.targetContext.packageManager.getLaunchIntentForPackage(GOOGLE_PLAY_PACKAGE)
+            device.targetContext.startActivity(intent)
+        }
+    }
+
+    companion object {
+
+        private const val GOOGLE_PLAY_PACKAGE = "com.android.vending"
+    }
+}
+
+

Let's launch the test. Test passed successfully, the Google Play app opens on the device.

+

Now we need to check that there is a button with the text Sign in on the opened screen. This is not our application, we do not have access to the source code, so getting the button id through the Layout Inspector will not work. You need to use other tools.

+

Tools for working with other applications

+

UIAutomator

+

UI Automator is a library for finding components on the screen and emulating user actions (clicks, swipes, text input, etc.). It allows you to manage the application the way the user would do it, i.e. to interact with any of its elements.

+

Thanks to this library, we can test any applications and perform various actions in them, despite the fact that we do not have access to their source code.

+
+

Info

+

You can read more about UiAutomator and its capabilities in documentation.

+
+

The Android SDK also includes the Ui Automator Viewer. It allows us to find the IDs of the elements we want to interact with, their position and other useful attributes.

+

In order to launch Ui Automator Viewer, you need to open a command line in the ../Android/sdk/tools/bin folder and execute the command uiautomatorviewer.

+

You should have a window like this:

+

UiAutomatorViewer first launch

+

If this did not happen and some error was displayed in the console, then you should google the error text.

+

The most common problem is that the Java version is not compatible with uiautomatorviewer. In this case, you should install Java 8 (use only released by Oracle) and set the path to it in environment variables. How to do this, we discussed in the lesson Executing adb commands.

+

Let's get back to writing the test. We will check the Google Play application, and in order to interact with it from the Ui Automator Viewer, you need to run it on the emulator and click on the Device Screenshot button:

+

UiAutomatorViewer create screenshot

+

On some OS versions, these icons are initially hidden, so if you don't see them, just stretch the program window.

+

On the right side, you can see all the information about the user interface elements. Now we are interested in the Sign in button. We click on this element and look at the information about the button:

+

UiAutomatorViewer button info

+

Here you can see some useful information:

+
    +
  1. Package is the name of the application package that we specified in the test. One way to find it out is through this program
  2. +
  3. Resource-id is the element id you can use to search for the button and interact with it from the test. In our case, it is not possible, because the id value indicates that the resource name has been obfuscated, that is, encrypted. Therefore, it is not possible to search for an element by id for this screen
  4. +
  5. Text - one way to find an element on the screen is by the text that is displayed on it. It turns out that we can find the button on this screen by the text attribute
  6. +
+ +

Developer Assistant

+

If for some reason you are not comfortable using the Ui Automator Viewer, or you are unable to launch it, then you can use the Developer Assistant application. It can be downloaded on Google Play.

+

After installing and launching Developer Assistant, you need to select it in the settings as the default assistant application. To do this, click on the Choose button and follow the instructions:

+

Developer Assistant Settings

+

Developer Assistant Settings

+

Developer Assistant Settings

+

Developer Assistant Settings

+

Developer Assistant Settings

+

Developer Assistant Settings

+

Once configured, you can run application analysis. Open the Google Play app and long press the Home button:

+

Developer Assistant Google play

+

You will see a window with information about the application, which you can move or expand if necessary. The App tab contains information about the application: package name, currently running Activity, etc.

+

Developer Assistant Google play

+

The Element tab allows you to explore the user interface elements.

+

Developer Assistant Google play

+

The Sign in button has all the same attributes that we saw in Ui Automator Viewer.

+

Dump

+

In some cases, which we'll talk about later in this tutorial, you won't be able to use the Developer Assistant because it can't display information about the system UI (notifications, dialogs, etc.). If you find yourself in such a situation that the Developer Assistant capabilities are not enough, and the Ui Automator Viewer failed to start, then there is a third option: run the adb shell command uiautomator dump.

+

To do this, on the emulator, open the screen that you need to get information about (in this case, Google Play). Open the console and run the command:

+
adb shell uiautomator dump
+
+

Uiautomator Dump

+

A window_dump.xml file should have appeared on your emulator, which can be found through the Device Explorer. If it is not displayed for you, then select the sdcard folder and click Synchronize:

+

Uiautomator Dump

+

If after these steps the file still does not appear, then run one more command in the console:

+
adb pull /sdcard/window_dump.xml
+
+

After that find the file on your computer via Device File Explorer and open it in Android Studio:

+

Uiautomator Dump

+

This file is a description of the screen in xml format. Here you can also find all the necessary objects, their properties and IDs. If you have it displayed in one line, then you should auto-format the file to make it easier to read the code. To do this, press the key combination ctrl + alt + L on Windows or cmd + option + L on Mac.

+

Uiautomator Dump

+

You can find the login button and see all its attributes. To do this, press the key combination ctrl + F (or cmd + F on Mac) and enter the text that is set on the "Sign in" button.

+

Uiautomator Dump

+

Writing a test

+

We have found the interface elements we need, and now we can start testing. As usual, we'll start by creating a Page Object for the Google Play screen.

+
package com.kaspersky.kaspresso.tutorial.screen
+
+object GooglePlayScreen {
+
+}
+
+

Previously, we inherited all Page Objects from the KScreen class. In this case, we needed to override two properties: layoutId and viewClass

+
override val layoutId: Int? = null
+override val viewClass: Class<*>? = null
+
+

We did this because we were testing the screen that is inside our application, we had access to the source code, the layout and the Activity we are working with. But now we want to test the screen from a third-party application, so it is impossible to search for some elements in it, click on buttons and perform any other actions with it the way that we did in previous lessons.

+

For these purposes, Kaspresso has the Kautomator component - a wrapper over the well-known UiAutomator tool. Kautomator makes writing tests much easier, and also adds a number of advantages compared to UiAutomator, which you can read about in detail in the Wiki.

+

Page objects for screens of third-party applications should not inherit from KScreen, but from UiScreen. Additionally, you need to override the packageName property so that it returns the package name of the application under test:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.components.kautomator.screen.UiScreen
+
+object GooglePlayScreen : UiScreen<GooglePlayScreen>() {
+
+    override val packageName: String = "com.android.vending"
+}
+
+

Further, all user interface elements will be instances of classes with the prefix Ui (UiButton, UiTextView, UiEditText...), and not K (KButton, KTextView, KEditText. ..) as it was before. The point is that we are currently testing another application and we need the functionality available in the Kautomator components.

+

On this screen, we are interested in the signIn button, add it:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.components.kautomator.component.text.UiButton
+import com.kaspersky.components.kautomator.screen.UiScreen
+
+object GooglePlayScreen : UiScreen<GooglePlayScreen>() {
+
+    override val packageName: String = "com.android.vending"
+
+    val signInButton = UiButton { }
+}
+
+

In curly brackets UiButton {...} we need to use some kind of matcher, thanks to which we will find the element on the screen. Previously, we used only withId, but now the id of the button is not available and we will have to use some other option.

+

To see all available matchers, you can go to the UiButton definition (hold ctrl and left-click on the class name). Inside it you will see the class UiViewBuilder.

+

UI Button

+

The UiViewBuilder class contains many matchers that you can use. By going to its definition (holding ctrl, left-clicking on the class name), you can see the full up-to-date list:

+

Matchers

+

For example, you can use withText to find the element containing specific text, or use withClassName to find an instance of some class.

+

Let's find the button by the text that is displayed on it.

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.components.kautomator.component.text.UiButton
+import com.kaspersky.components.kautomator.screen.UiScreen
+
+object GooglePlayScreen : UiScreen<GooglePlayScreen>() {
+
+    override val packageName: String = "com.android.vending"
+
+    val signInButton = UiButton { withText("Sign in") }
+}
+
+

We can add a test. Let's check that the login button is displayed on the Google Play screen:

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.GooglePlayScreen
+import org.junit.Test
+
+class GooglePlayTest : TestCase() {
+
+    @Test
+    fun testNotSignIn() = run {
+        step("Open Google Play") {
+            with(device.targetContext) {
+                val intent = packageManager.getLaunchIntentForPackage(GOOGLE_PLAY_PACKAGE)
+                startActivity(intent)
+            }
+        }
+        step("Check sign in button visibility") {
+            GooglePlayScreen {
+                signInButton.isDisplayed()
+            }
+        }
+    }
+
+    companion object {
+
+        private const val GOOGLE_PLAY_PACKAGE = "com.android.vending"
+    }
+}
+
+

Let's launch the test. It passed successfully.

+

Testing the system UI

+

We have considered one option when we need to use the UI automator for testing: if we are interacting with a third-party application. But this is not the only case when it should be used.

+

Let's open our tutorial application and go to the Notification Activity screen:

+

Notification Activity Button

+

Click on the “Show notification” button - a notification is displayed on top.

+
+

Info

+

You can read more about notifications in Android here.

+
+

Notification Shown

+

Let's try to test this screen.

+

First, let's create a Page Object for the screen with the "Show Notification" button. This screen is in our application, so we can inherit from KScreen. Button id can be found through the Layout Inspector:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.text.KButton
+
+object NotificationActivityScreen : KScreen<NotificationActivityScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val showNotificationButton = KButton { withId(R.id.show_notification_button) }
+}
+
+

In the Page Object of the main screen, add a button to open NotificationActivity:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.text.KButton
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
+    val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }
+    val loginActivityButton = KButton { withId(R.id.login_activity_btn) }
+    val notificationActivityButton = KButton { withId(R.id.notification_activity_btn) }
+}
+
+

You can create a test, first just show a notification by clicking on the button on the main screen:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.NotificationActivityScreen
+import org.junit.Rule
+import org.junit.Test
+
+class NotificationActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkNotification() = run {
+        step("Open notification activity") {
+            MainScreen {
+                notificationActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Show notification") {
+            NotificationActivityScreen {
+                showNotificationButton.isVisible()
+                showNotificationButton.isClickable()
+                showNotificationButton.click()
+            }
+        }
+    }
+}
+
+

Let's launch the test. It passed successfully, notification is displayed.

+

Now let's check that the title and content of the notification contain the required text.

+

Finding the id of the elements using the Layout Inspector or Developer Assistant will not work, because display of notifications belongs to the system UI. In this case, we will have to use one of two options: launch the Ui Automator Viewer and look through it, or run the adb shell uiautomator dump command.

+

Next, we will show the solution through the Ui Automator Viewer, and also attach a screenshot of where to find the View elements in the window_dump.xml file

+

Open the list of notifications and take a screenshot:

+

Ui automator notification

+

Using the dump command, the necessary elements can be found as follows

+

Dump

+

Dump

+

Here, by the package name, you can see that the notification drawer does not belong to our application, so for testing it is necessary to inherit from the UiScreen class and use Kautomator.

+

Create a Page Object of the notification screen:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.components.kautomator.screen.UiScreen
+
+object NotificationScreen : UiScreen<NotificationScreen>() {
+
+    override val packageName: String = "com.android.systemui"
+}
+
+

packageName was set to the value obtained by dump or Ui Automator Viewer.

+

We declare the elements with which we will interact.

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.components.kautomator.component.text.UiTextView
+import com.kaspersky.components.kautomator.screen.UiScreen
+
+object NotificationScreen : UiScreen<NotificationScreen>() {
+
+    override val packageName: String = "com.android.systemui"
+
+    val title = UiTextView { }
+    val content = UiTextView { }
+}
+
+

You can find elements by different criteria, for example, by text or by id. Let's find an element by its id. Call matcher withId:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.components.kautomator.component.text.UiTextView
+import com.kaspersky.components.kautomator.screen.UiScreen
+
+object NotificationScreen : UiScreen<NotificationScreen>() {
+
+    override val packageName: String = "com.android.systemui"
+
+    val title = UiTextView { withId("", "") }
+    val content = UiTextView { withId("", "") }
+}
+
+

The first parameter to pass is the package name of the application in whose resources the element will be searched. We could pass the previously obtained packageName and resource_id values:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.components.kautomator.component.text.UiTextView
+import com.kaspersky.components.kautomator.screen.UiScreen
+
+object NotificationScreen : UiScreen<NotificationScreen>() {
+
+    override val packageName: String = "com.android.systemui"
+
+    val title = UiTextView { withId(this@NotificationScreen.packageName, "android:id/title") }
+    val content = UiTextView { withId(this@NotificationScreen.packageName, "android:id/text") }
+}
+
+

But in this case, the elements will not be found. The id scheme of the element we are looking for on the screen of another application looks like this: package_name:id/resource_id. This string will be formed from the two parameters that we passed to the withId method. Instead of package_name the package name com.android.systemui will be substituted, instead of resource_id the identifier android:id/title will be substituted. The resulting resource_id will look like this: com.android.systemui:id/android:id/title. It turns out that the characters :id/ will be added for us, and we only need to pass what is to the right of the slash, which will be the correct identifier:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.components.kautomator.component.text.UiTextView
+import com.kaspersky.components.kautomator.screen.UiScreen
+
+object NotificationScreen : UiScreen<NotificationScreen>() {
+
+    override val packageName: String = "com.android.systemui"
+
+    val title = UiTextView { withId(this@NotificationScreen.packageName, "title") }
+    val content = UiTextView { withId(this@NotificationScreen.packageName, "text") }
+}
+
+

Now the full resource_id looks like this: com.android.systemui:id/title and com.android.systemui:id/text.

+

Please note that the first part (package_name) is different from what is specified in the Ui Automator Viewer, we specified the package name com.android.systemui, and the program says android.

+

Ui automator package

+

The reason is that each application can have its own resources, in which case the first part of the resource identifier will contain the package name of the application where the resource was created, and the application can also use the resources of the Android system. They are shared between different applications and contain the package name android.

+

This is exactly the case, so we specify android as the first parameter.

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.components.kautomator.component.text.UiTextView
+import com.kaspersky.components.kautomator.screen.UiScreen
+
+object NotificationScreen : UiScreen<NotificationScreen>() {
+
+    override val packageName: String = "com.android.systemui"
+
+    val title = UiTextView { withId("android", "title") }
+    val content = UiTextView { withId("android", "text") }
+}
+
+

Now we can add checks to this screen. Let's make sure that the correct texts are set in the title and in the body of the notification:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.NotificationActivityScreen
+import com.kaspersky.kaspresso.tutorial.screen.NotificationScreen
+import org.junit.Rule
+import org.junit.Test
+
+class NotificationActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun checkNotification() = run {
+        step("Open notification activity") {
+            MainScreen {
+                notificationActivityButton {
+                    isVisible()
+                    isClickable()
+                    click()
+                }
+            }
+        }
+        step("Show notification") {
+            NotificationActivityScreen {
+                showNotificationButton.isVisible()
+                showNotificationButton.isClickable()
+                showNotificationButton.click()
+            }
+        }
+        step("Check notification texts") {
+            NotificationScreen {
+                title.isDisplayed()
+                title.hasText("Notification Title")
+                content.isDisplayed()
+                content.hasText("Notification Content")
+            }
+        }
+    }
+}
+
+

Let's launch the test. It passed successfully.

+

Summary

+

In this lesson, we learned how to run tests for third-party applications, and also learned how you can test the system UI using UiAutomator, or rather its wrapper Kautomator. In addition, we got to know the programs that allow us to analyze the UI of applications, even if we do not have access to their source code: these are Ui Automator Viewer, Developer Assistant and UiAutomator Dump.

+


+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Wifi_sample_test/index.html b/Tutorial/Wifi_sample_test/index.html new file mode 100644 index 000000000..c23a0dde8 --- /dev/null +++ b/Tutorial/Wifi_sample_test/index.html @@ -0,0 +1,1680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 5. Testing the Internet connection and working with the Device class - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Testing the Internet connection and working with the Device class

+

In this tutorial we'll create a test that tests the Internet Availability (WifiActivity) screen.

+

Run our tutorial application and click on the Internet Availability button

+

Button Internet Availability

+

Manual testing

+

Let's manually test this screen first.

+

Initially, we have a CHECK WIFI STATUS button, there is no more text on the screen. Wifi is currently enabled on the device.

+

Launch Wifi Test Activity

+

Launch Wifi Test Activity

+

Let's click on the button.

+

Wifi enabled

+

This button is clickable, after clicking, the correct Wifi state status is displayed - enabled. Disable WiFi.

+

Turn-off wifi

+

Click on the button again and check the Wifi status now:

+

Wifi disabled

+

The state is determined correctly. One last check - let's flip the device over and make sure the text on the screen is preserved.

+

Wifi disabled landscape

+

The text is saved successfully, all tests passed. Now we need to achieve the same result with all the checks performed automatically.

+

Writing autotests

+

During the test, you will need to automatically turn the Internet on and off, as well as change the orientation of the device to landscape. This is beyond the responsibility of our application, which means that we will have to use adb commands for tests. This requires the ADB server to be running. We discussed this point in the previous lesson. If you forgot how to do it, you can review it again.

+

Now in our test, you will need to click on the Internet Availability button on the main screen. This means that it is necessary to modify the Page Object of the main screen by adding one more button there:

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.text.KButton
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
+    val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }
+}
+
+

Now we can add a new test class. In the same package where we have other tests, we add WifiSampleTest:

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+
+class WifiSampleTest: TestCase() {
+
+}
+
+

To check the Internet availability screen, you need to go to it. To do this, we will follow the same steps as in tutorial, in which we wrote our first autotest:

+
    +
  1. Add an activityRule so that when the test starts, we open MainActivity
  2. +
  3. Check that the button to go to the Internet check screen is visible and clickable
  4. +
  5. Click on the "Internet Availability" button
  6. +
+ +
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            wifiActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+    }
+}
+
+

Let's launch the test. It passed successfully. The Wifi test screen starts. Now we can test it.

+

To fully test this screen, we will need to change the Wifi connection state, as well as change the orientation of the device. To do this, in the BaseTestCase class (from which our WifiSampleTest class is inherited) there is an instance of the Device class, which is called device. We already encountered it in the previous lesson when we got the packageName of our application.

+

This object has many useful methods, which you can read about in detail here.

+

First of all, we are interested in a method that enables / disables the Internet. The network object, which is in the Device class, is responsible for working with the network.

+

If we want to change the Wifi state, we can do it like this:

+
/**
+* As a parameter, we pass the boolean type, false if we want to turn Wifi off, true if we want to turn it on
+*/
+device.network.toggleWiFi(false)
+
+

In addition to Wifi, we can also manage the mobile network, as well as the Internet connection on the device as a whole (Wifi + mobile network). In order to see all the available methods, you can go to the documentation above, but there is an easier way - put a dot after the name of the object and see which methods can be called on this object. It is usually clear what they do from their names.

+

Available methods

+

Let's write a test that performs all the necessary checks, except for flipping the device - we'll deal with flipping a bit later. The first step is to create a Page Object for the internet connection test screen WifiScreen. Add it to the com.kaspersky.kaspresso.tutorial.screen package

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.text.KButton
+import io.github.kakaocup.kakao.text.KTextView
+
+object WifiScreen : KScreen<WifiScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val checkWifiButton = KButton { withId(R.id.check_wifi_btn) }
+    val wifiStatus = KTextView { withId(R.id.wifi_status) }
+}
+
+

Now add steps:

+
    +
  1. Check if the button is visible and clickable
  2. +
  3. Check that the title contains no text
  4. +
  5. Click on the button
  6. +
  7. Checking that the title text is "enabled"
  8. +
  9. Disable Wifi
  10. +
  11. Click on the button
  12. +
  13. Checking that the title text is "disabled"
  14. +
+ +
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            wifiActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+        WifiScreen {
+            checkWifiButton.isVisible()
+            checkWifiButton.isClickable()
+            wifiStatus.hasEmptyText()
+            device.network.toggleWiFi(true)
+            checkWifiButton.click()
+            wifiStatus.hasText("enabled")
+            device.network.toggleWiFi(false)
+            checkWifiButton.click()
+            wifiStatus.hasText("disabled")
+        }
+    }
+}
+
+

We remember that it is not recommended to use hardcoded strings, it is better to use string resources instead.

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            wifiActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+        WifiScreen {
+            checkWifiButton.isVisible()
+            checkWifiButton.isClickable()
+            wifiStatus.hasEmptyText()
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.enabled_status)
+            device.network.toggleWiFi(false)
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.disabled_status)
+        }
+    }
+}
+
+
+

Info

+

Do not forget to enable Wifi on the device before starting the test, because after each launch it will be turned off for you and the test will fail on the second run.

+
+

Now we need to learn how to flip the device in order to perform the rest of the checks. The exploit object from the Device class is responsible for flipping the device, which you can also read more about in documentation.

+

The whole test process will now look like this:

+
    +
  1. Set device to portrait orientation
  2. +
  3. Check that the button is visible and clickable
  4. +
  5. Check that the title does not contain text
  6. +
  7. Click on the button
  8. +
  9. Check that the title text is "enabled"
  10. +
  11. Disable Wifi
  12. +
  13. Click on the button
  14. +
  15. Check that the title text is "disabled"
  16. +
  17. Flip the device
  18. +
  19. Check that the title text is still "disabled"
  20. +
+ +

package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.device.exploit.Exploit
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.WifiScreen
+import org.junit.Rule
+import org.junit.Test
+
+class WifiSampleTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            wifiActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+        WifiScreen {
+            device.exploit.setOrientation(Exploit.DeviceOrientation.Portrait)
+            checkWifiButton.isVisible()
+            checkWifiButton.isClickable()
+            wifiStatus.hasEmptyText()
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.enabled_status)
+            device.network.toggleWiFi(false)
+            checkWifiButton.click()
+            wifiStatus.hasText(R.string.disabled_status)
+            device.exploit.rotate()
+            wifiStatus.hasText(R.string.disabled_status)
+        }
+    }
+}
+
+Let's launch the test. It passed successfully.

+

Summary

+

In this lesson we practiced with the device object, learned how to change the status of the Internet connection and the screen orientation from the test code. Test passed and all checks completed successfully, but there are several serious problems in our code:

+
    +
  • The test is not broken into steps. As a result, we have a large canvas of code, which is quite difficult to understand
  • +
  • The test only succeeds if we have previously enabled internet on the device. At the same time, at each next start, the test will fall due to the fact that Wifi is turned off inside it
  • +
+ +

In the following lessons, we will learn how we can improve this code and solve the problems that have arisen.

+


+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Working_with_adb/index.html b/Tutorial/Working_with_adb/index.html new file mode 100644 index 000000000..035a39cd4 --- /dev/null +++ b/Tutorial/Working_with_adb/index.html @@ -0,0 +1,1713 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 4. Working with adb - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Working with adb

+

In the last lesson, we wrote the first test on Kaspresso, and at this stage, our test can interact with the elements of the application interface, can somehow influence them (e.g. click on the button) and check their state (visibility, clickability and etc.).

+

But often it is not enough to use only the capabilities of our application for testing. For example, during a test, we might want to test the operation of the application in various external states:

+
    +
  • When there is no Internet
  • +
  • During an incoming call
  • +
  • With a low battery
  • +
  • When changing device orientation
  • +
  • Etc.
  • +
+ +

In all of the above scenarios, the test must control the device and execute commands that are outside the responsibility of the application we are testing. In these cases, we can use the Android Debug Bridge (ADB) capabilities.

+

ADB is a command line tool that allows you to interact with your device through various commands. They can help you perform actions such as installing and removing programs, getting a list of installed applications, starting a specific Activity, turning off your Internet connection, and much more.

+

We can execute all adb commands ourselves through the command line, but the Kaspresso library supports working with adb and can execute them automatically. Adb-server needs to be started so that tests that work with adb can run.

+

Check java and adb

+

The process of launching adb-server is very simple, if the paths to java and adb are correctly registered on your computer. But if the paths are not registered, then they will have to be registered. Therefore, the first thing we will do is check if any additional work is required or if you already have everything ready to start adb-server.

+

Open a command prompt.

+

On Windows the key combination is Win + R, in the window that opens, enter cmd and press Enter.

+

Open cmd on windows 1

+

Open cmd on windows 2

+

First, we check that the path to java is correct. To do this, we write java -version.

+

If everything is fine, then you will see the installed version of Java.

+

Java version showed

+

If the paths are written incorrectly, you will see something similar to this:

+

Java version failed

+

Now we do the same check for adb. We print in the console adb version.

+

If everything is fine, then you will see your ADB version.

+

Adb version success

+

Otherwise, you will see something like this error:

+

Adb version failed

+

If everything works for you on both points, then you can skip the next step.

+

Setting up java and adb

+

The solution to these problems may differ depending on your operating system and some other factors, so we will present here the most popular solution for OS Windows. If you have a different OS, or for some reason this solution does not help you, then search the Internet for information on how to do the steps below in your situation. Without solving these problems, you will not be able to start adb-server and the tests will not work.

+

If you have reached this lesson, then you have successfully launched the application from Android Studio on the emulator, which means that java and adb are installed on your computer. The system simply does not know where to look for these programs. What needs to be done is to find the location of these programs and register the paths to them in the system.

+

We are looking for the path to java, usually it is located in the jre\bin folder (in some versions it will be located in jbr\bin). It can often be found at C:\Program Files\Java\jre1.8.0\bin.

+

If it is there, copy this path, if not, open Android Studio. Go to File -> Settings -> Build, Execution, Deployment -> Build Tools -> Gradle.

+

Show jsdk path in android studio

+

The path to the desired folder will be written here, and you can copy it.

+

Now it needs to be registered in the environment variables, for this press win + x -> select System -> Advanced System Settings -> Advanced -> Environment Variables.

+

Show system variables

+

In the System Variables section, select Path and click Edit -> New -> Paste the copied path to the folder with java -> Click OK.

+

Java bin path

+

Restart the computer for the changes to take effect and check the java -version command again.

+

Java version success

+

It remains for us to do the same for adb. We are looking for the path to the platform-tools folder, which contains adb.

+

Open Android Studio -> Tools -> SDK Manager. The Android SDK Location field contains the path to the Sdk folder, which contains platform-tools.

+

Copy this path and add it to System Variables as we did earlier with java.

+

Adb path

+

Restart the computer and check the adb version command.

+

Adb version success

+

Now we can start running adb-server. If the java and adb commands still do not work for you, then google it, there are a lot of options for solving the problem. All you need to do is find the path to java and adb and set them to environment variables.

+

Try different commands

+

Before running the tests, let's see what adb can do and look at a few commands.

+

First, we can see what devices are currently connected to adb. To do this, enter the command adb devices.

+

Empty devices list

+

So far we have not connected any devices to adb, so the list is empty. Let's run the application on the emulator and run the command again.

+

Devices list

+

Now our emulator is displayed in the list of devices.

+

With adb commands we can:

+
    +
  • Reboot device
  • +
  • Install an application
  • +
  • Remove an application
  • +
  • Upload files from/to a phone
  • +
  • etc.
  • +
+ +

For practice, let's remove the tutorial app we just launched. This is done with the command adb uninstall package_name.

+

Uninstall app

+

The most interesting tasks can be performed by running the adb shell command. It invokes the Android console (shell) to execute Linux commands on the device.

+

Open shell console

+

Here are some examples of such commands.

+

Getting a list of all installed applications pm list packages.

+

List packages

+

Please note that we first started the shell-console, and then wrote commands, already being in it. Therefore, at the current stage, other adb commands will not work for you until you close the shell console through the exit command.

+

Exit shell console

+

At the same time, you can execute shell-commands without opening a shell-console. To do this, specify the full name of the command along with adb shell. For example, let's try to take a screenshot and save it to the device. In Android Studio, you can open File Explorer, which displays all the files and folders on the device.

+

Device file explorer

+

Screenshots are usually saved on sdcard, we will do the same.

+

To create a screenshot, use the adb shell screencap /{pathToFile}/{name_of_image.png} command. In our case, it will look like this: adb shell screencap /sdcard/my_screen.png.

+

Create screenshot

+

In Device File Explorer, right-click and press Synchronize, after which the screenshot we created will be displayed in the folder.

+

Success screenshot

+

Working with adb in autotests

+

So, we've had a little practice with adb, now we need to learn how to work with it during the test run. That is, the test that we will create must be able to run adb commands and check the operation of the application after executing these commands.

+

In order for the tests to be able to execute adb commands, we need to run adb-server on our computer. First you need to download the adbserver-desktop.jar file on the official Kaspresso github and run the following command in the terminal:

+
java -jar <path/to/file>/adbserver-desktop.jar
+
+

In order for the path to the file to be correctly written in the console, it is enough to write the java -jar command and simply drag the adbserver-desctop.jar file to the console, the path to the file will be inserted automatically.

+

Drag server

+

After entering the command, press Enter. AdbServer will start. When running the test, the device will tell the desktop the necessary adb commands to run the test.

+

Launch Server

+

We can start creating an autotest.

+

Create a new AdbTest class in the com.kaspersky.kaspresso.tutorial package and inherit from the TestCase class.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+
+class AdbTest : TestCase() {
+}
+
+

Kaspresso has a special abstraction AdbServer for working with adb. An instance of this class is available in BaseTestContext and in BaseTestCase, which our AdbTest class inherits.

+

Earlier in the console, we ran the adb devices command, which displayed a list of connected devices. Let's run the same command with a test. Create a test() method and annotate it with @Test.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Test
+
+class AdbTest : TestCase() {
+
+    @Test
+    fun test() {
+
+    }
+}
+
+

To execute an adb command, we can access the adbServer field directly and call one of the methods - performAdb, performCmd or performShell. The names of the methods should make it clear what they do.

+
    +
  • `performAdb` executes an adb command
  • +
  • `performShell` executes a shell command
  • +
  • `performCmd` executes a command line
  • +
+ +

Now we want to call the adb command devices call the appropriate method adbServer.performAdb("devices").

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Test
+
+class AdbTest : TestCase() {
+
+    @Test
+    fun test() {
+        adbServer.performAdb("devices")
+    }
+}
+
+

Run the test. Test completed successfully. Please note that in order to run this test, you must meet 2 conditions:

+
    +
  1. adb-server is running
  2. +
  3. the application you are testing must have permission to use the Internet in the manifest
  4. +
+ +

We dealt with the first point earlier, now let's deal with the second. Every application that interacts with the Internet must contain a permission to use the Internet. It is written in the manifest.

+

Manifest Location

+

If you forget to specify this permission, the test will not work.

+

Now the test runs the adb command, but does not check the result of its execution. This adb devices command returns a list of resulting strings (type List<String>). At the moment, this collection (list of strings) contains only one line like this: exitCode=0, message=List of devices attached emulator-5555 device. Let's add a check that the first (and only) element of this collection contains the word "emulator", just to practice and make sure we get the output of the adb command correctly.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Assert // This class needs to be imported
+import org.junit.Test
+
+class AdbTest : TestCase() {
+
+    @Test
+    fun test() {
+        val result = adbServer.performAdb("devices")
+        Assert.assertTrue( // Method Assert.assertTrue() can be used to check if some condition is met, pay attention to the imports
+            Assert.assertTrue("emulator" in result.first()) // method 'in' checks that the first element of the result list contains the word "emulator"
+        ) 
+    }
+}
+
+

Let's launch the test. It passed successfully.

+

Now let's try to execute a non-existent adb command. First, let's see how its execution looks in the terminal. Let's execute adb undefined_command.

+
+

Info

+

Please note that adb-server is currently running in the terminal, if we want to work with the command line while the server is running, we need to launch another terminal window and work in it

+
+

Undefined command

+

When executing this command inside the test, it will throw an AdbServerException exception and the message field will contain a string with the text that we saw in the console: unknown command undefined_command. To prevent the test from failing, we need to handle this exception in a try catch block, and inside the catch block, we can add a check that the error message really contains the text specified above.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.internal.exceptions.AdbServerException
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Assert
+import org.junit.Test
+
+class AdbTest : TestCase() {
+
+    @Test
+    fun test() {
+        val result = adbServer.performAdb("devices")
+        Assert.assertTrue("emulator" in result.first())
+
+        val command = "undefined_command"
+        try {
+            adbServer.performAdb(command)
+        } catch (e: AdbServerException) {
+            Assert.assertTrue("unknown command $command" in e.message)
+        }
+    }
+}
+
+

Let's launch the test. It passed successfully.

+

We learned how to run adb commands inside tests. Let's practice adb shell commands. Previously, we got a list of installed applications using a query like adb shell pm list packages. Now we will execute it inside the test and check that our application is in the list of installed ones.

+
val packages = adbServer.performShell("pm list packages")
+Assert.assertTrue("com.kaspersky.kaspresso.tutorial" in packages.first())
+
+

Note that if we call a shell command with performShell, then we don't need to write adb shell.

+

Now we have hardcoded the name of the application package, but there is a much more convenient way. Inside the tests we can interact with the Device object, get some information about the device, the current application, and much more. From this object, we can get the package name of the current application. To do this, you need to access the targetContext property of the device object and get packageName from the context. The test code in this case will change to this:

+
...
+val packages = adbServer.performShell("pm list packages")
+Assert.assertTrue(device.targetContext.packageName in packages.first())
+...
+
+

Let's launch the test. It passed successfully.

+

The last type of commands that we will look at in this lesson are [cmd commands]. These are the commands that we write in the console. For example, to run an adb command, we write adb command_name in the console. Now, if we call performCmd instead of performAdb in the test, then we will need to write the entire command:

+
val result = adbServer.performCmd("adb devices")
+Assert.assertTrue("emulator" in result.first())
+
+

In this case, the result of the program will not change.

+

For practice, we can execute some cmd-command. For example, hostname prints the name of the host (your computer). If we run it in the console, the result will be something like this:

+

Hostname

+

Let's execute the same command inside the test and check that the result is not empty.

+
val hostname = adbServer.performCmd("hostname")
+Assert.assertTrue(hostname.isNotEmpty())
+
+

Let's launch the test. It passed successfully.

+

One of the tests we have previously written checks if there is an emulator in the list of connected devices.

+
val result = adbServer.performCmd("adb devices")
+Assert.assertTrue("emulator" in result.first())
+
+

We added it just for reference purposes, and to practice different commands. Real tests can be run both on emulators and on real devices, and tests should not crash because of this, so we will delete this test. The resulting AdbTest code will look like this:

+

package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.internal.exceptions.AdbServerException
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Assert
+import org.junit.Test
+
+class AdbTest : TestCase() {
+
+    @Test
+    fun test() {
+        val command = "undefined_command"
+        try {
+            adbServer.performAdb(command)
+        } catch (e: AdbServerException) {
+            Assert.assertTrue("unknown command $command" in e.message)
+        }
+
+        val packages = adbServer.performShell("pm list packages")
+        Assert.assertTrue(device.targetContext.packageName in packages.first())
+
+        val hostname = adbServer.performCmd("hostname")
+        Assert.assertTrue(hostname.isNotEmpty())
+    }
+}
+
+
+

+

Summary

+

In this lesson, we learned what adb is, set up adb-server operation, learned how to execute various types of commands (cmd, adb, shell) in the console and in autotests, and also learned about the Device object, from which we can receive various information about the device and application we are testing.

+


+

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/Writing_simple_test/index.html b/Tutorial/Writing_simple_test/index.html new file mode 100644 index 000000000..bdeaee5f5 --- /dev/null +++ b/Tutorial/Writing_simple_test/index.html @@ -0,0 +1,1896 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 3. Writing your first Kaspresso test - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Writing your first Kaspresso test

+

Switch to the desired branch in GIT

+

In Android Studio you can switch between branches and thus see different versions of a project. Initially, after downloading Kaspresso, you will be in the master branch.

+

Master branch

+

This branch contains the source code of the application, which we will cover with tests. In the current and subsequent lessons, step-by-step instructions for writing autotests will be given in codelabs format. The final result with all written tests is available in the TECH-tutorial-results branch, you can switch to it at any time and see the solution.

+

To do this, click on the name of the branch you are on, and in the search, enter the name of the branch you are interested in.

+

Switch to results

+

Manual testing

+

Before we start writing a test, let's take a closer look at the functionality that we will cover with autotests. To do this, switch to the 'master' branch.

+

Open configuration selection (1) and select tutorial (2):

+

Select tutorial

+

Check that the desired device is selected (1) and run the application (2):

+

Launch tutorial

+

After successfully launching the application, we will see the main screen of the Tutorial application.

+

Tutorial main

+

Click on the button with the text "Simple test" and see the following screen:

+

Page object example

+

The screen consists of:

+
    +
  1. +

    Header TextView

    +
  2. +
  3. +

    EditText input fields

    +
  4. +
  5. +

    Buttons

    +
  6. +
+
+

Info

+

A full list of widgets in Android with detailed information can be found here.

+
+

When you click on the button, the text in the header changes to the one entered in the input field.

+

Automatic testing

+

We manually checked that the result of the application meets the expectations:

+
    +
  1. On the main screen there is a button to go to the `SimpleTest` screen (the rest of the elements on this screen do not interest us for now)
  2. +
  3. This button is visible
  4. +
  5. This button is clickable
  6. +
  7. Clicking on it takes us to the SimpleTest screen
  8. +
  9. `SimpleTest` screen has three UI elements: title, input field and button
  10. +
  11. All these elements are visible
  12. +
  13. Header contains default text
  14. +
  15. If you enter some text in the input field and click on the button, then the text in the title changes to the entered one
  16. +
+ +

Now we need to write all the same checks in the code so that they are performed automatically.

+

To cover the application with Kaspresso tests, you need to start by including the Kaspresso library in the project dependencies.

+

Including Kaspresso in the project

+

Switch the display of the project files to Project (1) and add the dependency to the existing dependencies section in the build.gradle file of the Tutorial module:

+

Tutorial build gradle

+
dependencies {
+    androidTestImplementation("com.kaspersky.android-components:kaspresso:1.5.1")
+    androidTestUtil("androidx.test:orchestrator:1.4.2")
+}
+
+

Let's start writing the test by creating a Page object for the current screen

+

We can start writing the code of our test. To do this, it is necessary to create a model (class) for each screen that participates in the test and, inside this model, declare all the interface elements (buttons, text fields, etc.) that make up the screen that the test will interact with. This approach is called Page Object and you can read more about it in the documentation.

+

In the first four steps of the test, we are interacting with the main screen, so the first step is to create a Page Object for the main screen.

+

We will work in the androidTest folder of the tutorial module. If you do not have this folder, then you need to create it by right-clicking on the src folder and selecting New -> Directory.

+

Create directory

+

Select the item androidTest/kotlin:

+

Name directory androidTest

+

Inside the kotlin folder, let's create a separate package in which we will store all Page Objects:

+

Create package

+

Creating a separate package does not affect the functionality, we do it just for convenience, so that all screen models are in one place. You can give the package any name (with a few exceptions), but it's common for tests to use the same name as the application itself. We can go to the MainActivity file and the package name will be listed at the top.

+

MainActivity Package name

+

Copy this name and paste it into the package name. Specifically, in this package we will store only screen models (Page Objects), so let's add .screen at the end.

+

Screen Package name

+

When we add other classes to the folder with tests, we will put them in other packages, but the first part of their name will be the same: com.kaspersky.kaspresso.tutorial.

+

Now in the created package we add a screen model (class):

+

Create class

+

Choose the type Object and name it MainScreen.

+

Create MainScreen

+

MainScreen is a model of the main screen. In order for this model to be used in autotests, it is necessary to inherit from the KScreen class and specify the name of this class in angle brackets.

+
+

Info

+

Specifying the type in angle brackets in Java and Kotlin is called Generics. You can read more about this in Java and Kotlin documentation.

+
+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+
+object MainScreen : KScreen<MainScreen>() {
+}
+
+

An error occurred because the KScreen class contains two members that need to be redefined when inheriting. In order to do this quickly in Android Studio, we can press the key combination ctrl + i and select the elements that we want to override.

+

Override methods

+

Holding ctrl, select all items and press OK.

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int?
+        get() = TODO("Not yet implemented")
+    override val viewClass: Class<*>?
+        get() = TODO("Not yet implemented")
+}
+
+

New lines of code appeared in the file. Instead of TODO, you need to write the correct implementation: the id of the layout (layoutId) that is set on the screen, and the name of the class (viewClass). This is necessary to associate the test with a specific layout file and activity class. This binding will make further support and refinement of the test more convenient, but for now we are faced with the task of writing the first test, so we will leave the null value.

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+}
+
+

Now inside the MainScreen class we will declare all the user interface elements with which the test will interact. In our case, we are only interested in the SimpleTest button on the main screen.

+

Override methods

+

In order for the test to interact with it, you need to know the id by which this button can be found on the screen. These identifiers are assigned by a developer when writing the application.

+

To find out what id has been assigned to some interface element, you can use the LayoutInspector tool built into Android Studio.

+
    +
  1. Launch the application
  2. +
  3. In the bottom right corner of Android Studio select Layout Inspector Find bottom layout inspector
  4. +
  5. Wait for the screen to load Layout inspector loaded
  6. +
  7. If the screen does not load, then check that you have the desired process selected Choose process
  8. +
+ +

Select an item on the screen and look for its id. This is the identifier that interests us.

+

Search for button id

+

It is also important to understand what UI element we are working with. To do this, you can go to the layout where the element was declared and see all the information about it.

+

Find layout

+

In this case, it's a Button element with id simple_activity_btn

+

Find button in layout

+

We can add this button to the MainScreen. Usually the name of the variable matches the element's id, but is written without underscores and each word except the first one is capitalized (this is called camelCase)

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val simpleActivityButton = 
+}
+
+

The simpleActivityButton variable needs to be assigned a value. It represents a button that can be tested, and the class KButton is responsible for this. This is how setting the value to this variable will look like, now we will analyze in detail what this code does.

+
package com.kaspersky.kaspresso.tutorial.screen
+
+import com.kaspersky.kaspresso.screens.KScreen
+import com.kaspersky.kaspresso.tutorial.R
+import io.github.kakaocup.kakao.text.KButton
+
+object MainScreen : KScreen<MainScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
+}
+
+

First, let's jump into the definition of KButton and see what it is. To do this, holding ctrl, click on the name of the KButton class with the left mouse button.

+

Find source of KButton

+

We see that this is a class that inherits from KBaseView and implements the TextViewAssertions interface. We can go to the definition of KBaseView and see all the inheritors of this class, there are quite a lot of them.

+

Find kbaseview children

+

Why are they all needed?

+

The reason is that each element of the user interface can be tested in different ways. For example, in a TextView we can check what text is currently set in it, we can set a new text, while the ProgressBar does not contain any text and it makes no sense to check what text is set in it.

+

Therefore, depending on which interface element we are testing, we need to choose the correct implementation of KBaseView. Now we are testing a button, so we chose KButton. On the next screen, we will test the title (TextView) and input field (EditText) and select the appropriate KBaseView implementations.

+

Show children which we need

+

Next, the test should find this button on the screen according to some criterion. In this case, we will search for an element by id, so we use the withId matcher, where we pass the button ID as a parameter, which we found thanks to the Layout Inpector.

+

In order to specify this id, we used the R.id... syntax, where R is the class with all the resources of the application. Thanks to it, you can find the id of interface elements, lines that are in the project, pictures, etc. When you enter the name of this class, Android Studio should import it automatically, but sometimes this does not happen, then you need to enter this import manually.

+
import com.kaspersky.kaspresso.tutorial.R
+
+

That's it, now we have a model of the main screen and this model contains a button that can be tested. We can start writing the test itself.

+

Add SimpleActivityTest

+

In the folder androidTest -> kotlin, in the package we created, add the class SimpleActivityTest.

+

Creating Test First part

+

Creating Test Second part

+

The new class was placed in the screen package, but we would like it to contain only screen models, so we will move the created test to the root of the com.kaspersky.kaspresso.tutorial package. In order to do this, right-click on the class name and select Refactor -> Move

+

Move to another package

+

And remove the last part .screen from the package name.

+

Change package name

+

The test class must be inherited from the TestCase class. Pay attention to imports, the TestCase class must be imported from the import com.kaspersky.kaspresso.testcases.api.testcase.TestCase package.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+
+class SimpleActivityTest: TestCase() {
+}
+
+

Then we add the test() method, in which we will check the operation of the application. It can have any name, not necessarily "test", but it needs to be annotated with @Test (import org.junit.Test).

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Test
+
+class SimpleActivityTest : TestCase() {
+
+    @Test
+    fun test() {
+
+    }
+}
+
+

The SimpleActivityTest test can be run. Information on how to run tests in Android Studio can be found in the previous tutorial.

+

Success passed test

+

For now this test does nothing, so it succeeds. Let's add logic to it and test the MainScreen.

+
package com.kaspersky.kaspresso.tutorial
+
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import org.junit.Test
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+
+class SimpleActivityTest : TestCase() {
+
+    @Test
+    fun test() {
+        MainScreen {
+            simpleActivityButton {
+                isVisible()
+                isClickable()
+            }
+        }
+    }
+}
+
+

Inside the test method, we get the MainScreen object, open the curly brackets and refer to the button that we will test, then open the curly brackets again and write all the checks here. Now, thanks to the isVisible() and isClickable() methods, we check that the button is visible and clickable. Let's launch the test. It falls.

+

Feailed test

+

The probleem is that Page Object MainScreen refers to MainActivity (this is the activity that the user sees when he launches the application) and, in order for the elements to be displayed on the screen, this activity must be launched before the test is executed. In order for some kind of activity to be launched before the test, you can add the following lines:

+
    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+

This test will launch the specified MainActivity activity before running the test and close it after the test runs.

+

You can read more about activityScenarioRule here.

+

Then the entire test code will look like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.MainActivity
+import org.junit.Rule
+import org.junit.Test
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+
+class SimpleActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            simpleActivityButton {
+                isVisible()
+                isClickable()
+            }
+        }
+    }
+}
+
+

Launch it. Everything is fine, our test is successful, and you can see on the device that during the test the activity we need opens and closes after the run.

+

Success test

+

It's good practice when writing tests to make sure that the test not only passes, but also fails if the condition is not met. This way you eliminate the situation when the tests are "green", but in reality, due to some error in the code, the tests were not performed at all. Let's do this by checking that the button contains incorrect text.

+
class SimpleActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            simpleActivityButton {
+                isVisible()
+                isClickable()
+                containsText("Incorrect text")
+            }
+        }
+    }
+}
+
+

The test fails, let's change the text to the correct one.

+
class SimpleActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            simpleActivityButton {
+                isVisible()
+                isClickable()
+                containsText("Simple test")
+            }
+        }
+    }
+}
+
+

The test is successful.

+

Now we need to test the SimpleActivity. We do it the same way as MainScreen: first, create a Page Object.

+
object SimpleActivityScreen : KScreen<SimpleActivityScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+}
+
+

Then look for id elements through the Layout Inspector:

+

Title id in inspector

+

Input id in inspector

+

Button id in inspector

+

Do not forget to specify correct View types: KTextView for the title, KEditText for the input field, and KButton for the button.

+
object SimpleActivityScreen : KScreen<SimpleActivityScreen>() {
+
+    override val layoutId: Int? = null
+    override val viewClass: Class<*>? = null
+
+    val simpleTitle = KTextView { withId(R.id.simple_title) }
+    val inputText = KEditText { withId(R.id.input_text) }
+    val changeTitleButton = KButton { withId(R.id.change_title_btn) }
+}
+
+

And now we can test this screen. In order to go to it, on the main screen you need to click on the "Simple Test" button, so we call click() in the code.

+

Add checks for this screen:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.MainActivity
+import org.junit.Rule
+import org.junit.Test
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.SimpleActivityScreen
+
+class SimpleActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            simpleActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+        SimpleActivityScreen {
+            simpleTitle.isVisible()
+            changeTitleButton.isClickable()
+            simpleTitle.hasText("Default title")
+            inputText.replaceText("new title")
+            changeTitleButton.click()
+            simpleTitle.hasText("new title")
+
+        }
+    }
+}
+
+

Our first test is almost ready. The only change worth making is that we're using the hardcoded "Default title" text here. For now, the test passes successfully, but if the application is suddenly localized into different languages, then when the test is launched with the English locale, the test can pass successfully, but if we run it on a device with the Russian locale, the test will fail.

+

So instead of hardcoding the string, we'll take it from the application's resources. In the activity's layout, we can see which line was used in this TextView.

+

Find string in layout

+

Go to string resources (file values/strings.xml) and copy the string id.

+

Find string in values folder

+

Now in the hasText method, instead of using the "Default title" string, we use its id R.string.simple_activity_default_title.

+

Don't forget to import the R resource class import com.kaspersky.kaspresso.tutorial.R.

+

The final test code looks like this:

+
package com.kaspersky.kaspresso.tutorial
+
+import androidx.test.ext.junit.rules.activityScenarioRule
+import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
+import com.kaspersky.kaspresso.tutorial.MainActivity
+import com.kaspersky.kaspresso.tutorial.R
+import org.junit.Rule
+import org.junit.Test
+import com.kaspersky.kaspresso.tutorial.screen.MainScreen
+import com.kaspersky.kaspresso.tutorial.screen.SimpleActivityScreen
+
+class SimpleActivityTest : TestCase() {
+
+    @get:Rule
+    val activityRule = activityScenarioRule<MainActivity>()
+
+    @Test
+    fun test() {
+        MainScreen {
+            simpleActivityButton {
+                isVisible()
+                isClickable()
+                click()
+            }
+        }
+        SimpleActivityScreen {
+            simpleTitle.isVisible()
+            changeTitleButton.isClickable()
+            simpleTitle.hasText(R.string.simple_activity_default_title)
+            inputText.replaceText("new title")
+            changeTitleButton.click()
+            simpleTitle.hasText("new title")
+
+        }
+    }
+}
+
+

Summary

+

In this tutorial, we have written our first Kaspresso test. In practice, we got acquainted with the PageObject approach. We learned how to get interface element IDs using the Layout inspector.

+


+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Create_device.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Create_device.png new file mode 100644 index 000000000..309b069b7 Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Create_device.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Device_name.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Device_name.png new file mode 100644 index 000000000..866006c1a Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Device_name.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Github_download_button.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Github_download_button.png new file mode 100644 index 000000000..23b23e197 Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Github_download_button.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Github_download_zip.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Github_download_zip.png new file mode 100644 index 000000000..ba3a8042d Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Github_download_zip.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Hyper_Visor.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Hyper_Visor.png new file mode 100644 index 000000000..ddb5363fe Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Hyper_Visor.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Hyper_Visor_next.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Hyper_Visor_next.png new file mode 100644 index 000000000..ef549b800 Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Hyper_Visor_next.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Launch_device.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Launch_device.png new file mode 100644 index 000000000..6afa04120 Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Launch_device.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/SDK_component_installer_finish.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/SDK_component_installer_finish.png new file mode 100644 index 000000000..7401966ed Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/SDK_component_installer_finish.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/SDK_component_installer_next.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/SDK_component_installer_next.png new file mode 100644 index 000000000..8a8f5c610 Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/SDK_component_installer_next.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/SDK_component_isntaller.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/SDK_component_isntaller.png new file mode 100644 index 000000000..4fef5bb70 Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/SDK_component_isntaller.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Select_hardware.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Select_hardware.png new file mode 100644 index 000000000..2e43d988e Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Select_hardware.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/System_Image.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/System_Image.png new file mode 100644 index 000000000..20616ba4e Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/System_Image.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Tools_Device_Manager.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Tools_Device_Manager.png new file mode 100644 index 000000000..d0b82bc56 Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/Tools_Device_Manager.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/clone_project.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/clone_project.png new file mode 100644 index 000000000..e7f2dbe2d Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/clone_project.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/download_by_git.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/download_by_git.png new file mode 100644 index 000000000..071912c0b Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/download_by_git.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/get_from_vcs.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/get_from_vcs.png new file mode 100644 index 000000000..cbf3813ba Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/get_from_vcs.png differ diff --git a/Tutorial/images/Download_Kaspresso_project_and_Android_studio/new_project_from_vcs.png b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/new_project_from_vcs.png new file mode 100644 index 000000000..ad6935fa7 Binary files /dev/null and b/Tutorial/images/Download_Kaspresso_project_and_Android_studio/new_project_from_vcs.png differ diff --git a/Tutorial/images/Running_the_first_test/device_select.png b/Tutorial/images/Running_the_first_test/device_select.png new file mode 100644 index 000000000..d8a50d26f Binary files /dev/null and b/Tutorial/images/Running_the_first_test/device_select.png differ diff --git a/Tutorial/images/Running_the_first_test/launch_test.png b/Tutorial/images/Running_the_first_test/launch_test.png new file mode 100644 index 000000000..92ec54b26 Binary files /dev/null and b/Tutorial/images/Running_the_first_test/launch_test.png differ diff --git a/Tutorial/images/Running_the_first_test/logcat.png b/Tutorial/images/Running_the_first_test/logcat.png new file mode 100644 index 000000000..e618856b8 Binary files /dev/null and b/Tutorial/images/Running_the_first_test/logcat.png differ diff --git a/Tutorial/images/Running_the_first_test/logcat_search.png b/Tutorial/images/Running_the_first_test/logcat_search.png new file mode 100644 index 000000000..23712adae Binary files /dev/null and b/Tutorial/images/Running_the_first_test/logcat_search.png differ diff --git a/Tutorial/images/Running_the_first_test/run_application.png b/Tutorial/images/Running_the_first_test/run_application.png new file mode 100644 index 000000000..83adc4281 Binary files /dev/null and b/Tutorial/images/Running_the_first_test/run_application.png differ diff --git a/Tutorial/images/Running_the_first_test/run_simple_test.png b/Tutorial/images/Running_the_first_test/run_simple_test.png new file mode 100644 index 000000000..6e33e7bae Binary files /dev/null and b/Tutorial/images/Running_the_first_test/run_simple_test.png differ diff --git a/Tutorial/images/Running_the_first_test/run_simple_test_1.png b/Tutorial/images/Running_the_first_test/run_simple_test_1.png new file mode 100644 index 000000000..e33f7bbaa Binary files /dev/null and b/Tutorial/images/Running_the_first_test/run_simple_test_1.png differ diff --git a/Tutorial/images/Running_the_first_test/run_simple_test_2.png b/Tutorial/images/Running_the_first_test/run_simple_test_2.png new file mode 100644 index 000000000..e33778e10 Binary files /dev/null and b/Tutorial/images/Running_the_first_test/run_simple_test_2.png differ diff --git a/Tutorial/images/Running_the_first_test/test_result.png b/Tutorial/images/Running_the_first_test/test_result.png new file mode 100644 index 000000000..2efd131e6 Binary files /dev/null and b/Tutorial/images/Running_the_first_test/test_result.png differ diff --git a/Tutorial/images/adb_lesson/adb_path.png b/Tutorial/images/adb_lesson/adb_path.png new file mode 100644 index 000000000..87f94c3f5 Binary files /dev/null and b/Tutorial/images/adb_lesson/adb_path.png differ diff --git a/Tutorial/images/adb_lesson/adb_version_failed.png b/Tutorial/images/adb_lesson/adb_version_failed.png new file mode 100644 index 000000000..05c8fe45f Binary files /dev/null and b/Tutorial/images/adb_lesson/adb_version_failed.png differ diff --git a/Tutorial/images/adb_lesson/adb_version_success.png b/Tutorial/images/adb_lesson/adb_version_success.png new file mode 100644 index 000000000..fc7fe75af Binary files /dev/null and b/Tutorial/images/adb_lesson/adb_version_success.png differ diff --git a/Tutorial/images/adb_lesson/bin_path.png b/Tutorial/images/adb_lesson/bin_path.png new file mode 100644 index 000000000..26bf43fff Binary files /dev/null and b/Tutorial/images/adb_lesson/bin_path.png differ diff --git a/Tutorial/images/adb_lesson/create_screenshot.png b/Tutorial/images/adb_lesson/create_screenshot.png new file mode 100644 index 000000000..8dbd6e2cd Binary files /dev/null and b/Tutorial/images/adb_lesson/create_screenshot.png differ diff --git a/Tutorial/images/adb_lesson/device_file_explorer.png b/Tutorial/images/adb_lesson/device_file_explorer.png new file mode 100644 index 000000000..cb961010f Binary files /dev/null and b/Tutorial/images/adb_lesson/device_file_explorer.png differ diff --git a/Tutorial/images/adb_lesson/devices_list.png b/Tutorial/images/adb_lesson/devices_list.png new file mode 100644 index 000000000..101eff51c Binary files /dev/null and b/Tutorial/images/adb_lesson/devices_list.png differ diff --git a/Tutorial/images/adb_lesson/drag_server.png b/Tutorial/images/adb_lesson/drag_server.png new file mode 100644 index 000000000..453063cbb Binary files /dev/null and b/Tutorial/images/adb_lesson/drag_server.png differ diff --git a/Tutorial/images/adb_lesson/empty_devices_list.png b/Tutorial/images/adb_lesson/empty_devices_list.png new file mode 100644 index 000000000..d2a4fc418 Binary files /dev/null and b/Tutorial/images/adb_lesson/empty_devices_list.png differ diff --git a/Tutorial/images/adb_lesson/exit_shell_console.png b/Tutorial/images/adb_lesson/exit_shell_console.png new file mode 100644 index 000000000..0a9c478d4 Binary files /dev/null and b/Tutorial/images/adb_lesson/exit_shell_console.png differ diff --git a/Tutorial/images/adb_lesson/hostname.png b/Tutorial/images/adb_lesson/hostname.png new file mode 100644 index 000000000..fadf4bdbe Binary files /dev/null and b/Tutorial/images/adb_lesson/hostname.png differ diff --git a/Tutorial/images/adb_lesson/java_version_failed.png b/Tutorial/images/adb_lesson/java_version_failed.png new file mode 100644 index 000000000..9e97cf235 Binary files /dev/null and b/Tutorial/images/adb_lesson/java_version_failed.png differ diff --git a/Tutorial/images/adb_lesson/java_version_success.png b/Tutorial/images/adb_lesson/java_version_success.png new file mode 100644 index 000000000..e5c8d1482 Binary files /dev/null and b/Tutorial/images/adb_lesson/java_version_success.png differ diff --git a/Tutorial/images/adb_lesson/jdk_in_android_studio.png b/Tutorial/images/adb_lesson/jdk_in_android_studio.png new file mode 100644 index 000000000..ab34f2798 Binary files /dev/null and b/Tutorial/images/adb_lesson/jdk_in_android_studio.png differ diff --git a/Tutorial/images/adb_lesson/launch_server.png b/Tutorial/images/adb_lesson/launch_server.png new file mode 100644 index 000000000..c0639bb8e Binary files /dev/null and b/Tutorial/images/adb_lesson/launch_server.png differ diff --git a/Tutorial/images/adb_lesson/list_packages.png b/Tutorial/images/adb_lesson/list_packages.png new file mode 100644 index 000000000..f10ae6bda Binary files /dev/null and b/Tutorial/images/adb_lesson/list_packages.png differ diff --git a/Tutorial/images/adb_lesson/manifest_location.png b/Tutorial/images/adb_lesson/manifest_location.png new file mode 100644 index 000000000..f68d5d998 Binary files /dev/null and b/Tutorial/images/adb_lesson/manifest_location.png differ diff --git a/Tutorial/images/adb_lesson/open_shell_console.png b/Tutorial/images/adb_lesson/open_shell_console.png new file mode 100644 index 000000000..d61a7c5f1 Binary files /dev/null and b/Tutorial/images/adb_lesson/open_shell_console.png differ diff --git a/Tutorial/images/adb_lesson/success_screen.png b/Tutorial/images/adb_lesson/success_screen.png new file mode 100644 index 000000000..3670fde11 Binary files /dev/null and b/Tutorial/images/adb_lesson/success_screen.png differ diff --git a/Tutorial/images/adb_lesson/system_variables.png b/Tutorial/images/adb_lesson/system_variables.png new file mode 100644 index 000000000..dff868aa5 Binary files /dev/null and b/Tutorial/images/adb_lesson/system_variables.png differ diff --git a/Tutorial/images/adb_lesson/undefined_command.png b/Tutorial/images/adb_lesson/undefined_command.png new file mode 100644 index 000000000..53cbc26ee Binary files /dev/null and b/Tutorial/images/adb_lesson/undefined_command.png differ diff --git a/Tutorial/images/adb_lesson/uninstall_app.png b/Tutorial/images/adb_lesson/uninstall_app.png new file mode 100644 index 000000000..75dabcbf7 Binary files /dev/null and b/Tutorial/images/adb_lesson/uninstall_app.png differ diff --git a/Tutorial/images/adb_lesson/windows_cmd_open_1.png b/Tutorial/images/adb_lesson/windows_cmd_open_1.png new file mode 100644 index 000000000..0aa81e12c Binary files /dev/null and b/Tutorial/images/adb_lesson/windows_cmd_open_1.png differ diff --git a/Tutorial/images/adb_lesson/windows_cmd_open_2.png b/Tutorial/images/adb_lesson/windows_cmd_open_2.png new file mode 100644 index 000000000..39c88f381 Binary files /dev/null and b/Tutorial/images/adb_lesson/windows_cmd_open_2.png differ diff --git a/Tutorial/images/flaky/flaky_1.png b/Tutorial/images/flaky/flaky_1.png new file mode 100644 index 000000000..087ccff96 Binary files /dev/null and b/Tutorial/images/flaky/flaky_1.png differ diff --git a/Tutorial/images/flaky/flaky_2.png b/Tutorial/images/flaky/flaky_2.png new file mode 100644 index 000000000..d87e8cb81 Binary files /dev/null and b/Tutorial/images/flaky/flaky_2.png differ diff --git a/Tutorial/images/flaky/flaky_3.png b/Tutorial/images/flaky/flaky_3.png new file mode 100644 index 000000000..be54c0962 Binary files /dev/null and b/Tutorial/images/flaky/flaky_3.png differ diff --git a/Tutorial/images/flaky/flaky_4.png b/Tutorial/images/flaky/flaky_4.png new file mode 100644 index 000000000..be2d0bcb7 Binary files /dev/null and b/Tutorial/images/flaky/flaky_4.png differ diff --git a/Tutorial/images/flaky/flaky_activity_btn.png b/Tutorial/images/flaky/flaky_activity_btn.png new file mode 100644 index 000000000..84a4cac9e Binary files /dev/null and b/Tutorial/images/flaky/flaky_activity_btn.png differ diff --git a/Tutorial/images/logs/advanced_builder.png b/Tutorial/images/logs/advanced_builder.png new file mode 100644 index 000000000..9a13e2583 Binary files /dev/null and b/Tutorial/images/logs/advanced_builder.png differ diff --git a/Tutorial/images/logs/after_auth.png b/Tutorial/images/logs/after_auth.png new file mode 100644 index 000000000..ebbdb7aff Binary files /dev/null and b/Tutorial/images/logs/after_auth.png differ diff --git a/Tutorial/images/logs/create_class.png b/Tutorial/images/logs/create_class.png new file mode 100644 index 000000000..1c634304f Binary files /dev/null and b/Tutorial/images/logs/create_class.png differ diff --git a/Tutorial/images/logs/create_package.png b/Tutorial/images/logs/create_package.png new file mode 100644 index 000000000..0303b1e68 Binary files /dev/null and b/Tutorial/images/logs/create_package.png differ diff --git a/Tutorial/images/logs/create_package_2.png b/Tutorial/images/logs/create_package_2.png new file mode 100644 index 000000000..ea4f0014e Binary files /dev/null and b/Tutorial/images/logs/create_package_2.png differ diff --git a/Tutorial/images/logs/custom_log.png b/Tutorial/images/logs/custom_log.png new file mode 100644 index 000000000..e1884366b Binary files /dev/null and b/Tutorial/images/logs/custom_log.png differ diff --git a/Tutorial/images/logs/custom_log_test.png b/Tutorial/images/logs/custom_log_test.png new file mode 100644 index 000000000..b39961d40 Binary files /dev/null and b/Tutorial/images/logs/custom_log_test.png differ diff --git a/Tutorial/images/logs/customized_builder.png b/Tutorial/images/logs/customized_builder.png new file mode 100644 index 000000000..9df6b88d0 Binary files /dev/null and b/Tutorial/images/logs/customized_builder.png differ diff --git a/Tutorial/images/logs/kaspresso_test_tag.png b/Tutorial/images/logs/kaspresso_test_tag.png new file mode 100644 index 000000000..647b11e55 Binary files /dev/null and b/Tutorial/images/logs/kaspresso_test_tag.png differ diff --git a/Tutorial/images/logs/logcat.png b/Tutorial/images/logs/logcat.png new file mode 100644 index 000000000..75fb9ab71 Binary files /dev/null and b/Tutorial/images/logs/logcat.png differ diff --git a/Tutorial/images/logs/login_activity.png b/Tutorial/images/logs/login_activity.png new file mode 100644 index 000000000..b0539938d Binary files /dev/null and b/Tutorial/images/logs/login_activity.png differ diff --git a/Tutorial/images/logs/main_screen.png b/Tutorial/images/logs/main_screen.png new file mode 100644 index 000000000..aaeda9dde Binary files /dev/null and b/Tutorial/images/logs/main_screen.png differ diff --git a/Tutorial/images/logs/screenshots.png b/Tutorial/images/logs/screenshots.png new file mode 100644 index 000000000..8ae6e61aa Binary files /dev/null and b/Tutorial/images/logs/screenshots.png differ diff --git a/Tutorial/images/logs/setup_password.png b/Tutorial/images/logs/setup_password.png new file mode 100644 index 000000000..1170a163a Binary files /dev/null and b/Tutorial/images/logs/setup_password.png differ diff --git a/Tutorial/images/logs/test_case_params.png b/Tutorial/images/logs/test_case_params.png new file mode 100644 index 000000000..5aad51445 Binary files /dev/null and b/Tutorial/images/logs/test_case_params.png differ diff --git a/Tutorial/images/logs/test_failed_1.png b/Tutorial/images/logs/test_failed_1.png new file mode 100644 index 000000000..ee8d1e1a5 Binary files /dev/null and b/Tutorial/images/logs/test_failed_1.png differ diff --git a/Tutorial/images/permissions/call_1.png b/Tutorial/images/permissions/call_1.png new file mode 100644 index 000000000..ad820d5db Binary files /dev/null and b/Tutorial/images/permissions/call_1.png differ diff --git a/Tutorial/images/permissions/deny_permission_settings.png b/Tutorial/images/permissions/deny_permission_settings.png new file mode 100644 index 000000000..8eae4e90a Binary files /dev/null and b/Tutorial/images/permissions/deny_permission_settings.png differ diff --git a/Tutorial/images/permissions/device_perm_methods.png b/Tutorial/images/permissions/device_perm_methods.png new file mode 100644 index 000000000..82cfa04ea Binary files /dev/null and b/Tutorial/images/permissions/device_perm_methods.png differ diff --git a/Tutorial/images/permissions/main_screen.png b/Tutorial/images/permissions/main_screen.png new file mode 100644 index 000000000..d562478ad Binary files /dev/null and b/Tutorial/images/permissions/main_screen.png differ diff --git a/Tutorial/images/permissions/make_call_screen.png b/Tutorial/images/permissions/make_call_screen.png new file mode 100644 index 000000000..e5dcca234 Binary files /dev/null and b/Tutorial/images/permissions/make_call_screen.png differ diff --git a/Tutorial/images/permissions/rename.png b/Tutorial/images/permissions/rename.png new file mode 100644 index 000000000..dea224268 Binary files /dev/null and b/Tutorial/images/permissions/rename.png differ diff --git a/Tutorial/images/permissions/rename_2.png b/Tutorial/images/permissions/rename_2.png new file mode 100644 index 000000000..b57a3c222 Binary files /dev/null and b/Tutorial/images/permissions/rename_2.png differ diff --git a/Tutorial/images/permissions/request_permission_1.png b/Tutorial/images/permissions/request_permission_1.png new file mode 100644 index 000000000..dce36a02d Binary files /dev/null and b/Tutorial/images/permissions/request_permission_1.png differ diff --git a/Tutorial/images/recycler_view/layout_inspector.png b/Tutorial/images/recycler_view/layout_inspector.png new file mode 100644 index 000000000..fea71e7bc Binary files /dev/null and b/Tutorial/images/recycler_view/layout_inspector.png differ diff --git a/Tutorial/images/recycler_view/main_screen.png b/Tutorial/images/recycler_view/main_screen.png new file mode 100644 index 000000000..9b48062ec Binary files /dev/null and b/Tutorial/images/recycler_view/main_screen.png differ diff --git a/Tutorial/images/recycler_view/removed.png b/Tutorial/images/recycler_view/removed.png new file mode 100644 index 000000000..fade38d9b Binary files /dev/null and b/Tutorial/images/recycler_view/removed.png differ diff --git a/Tutorial/images/recycler_view/swiped.png b/Tutorial/images/recycler_view/swiped.png new file mode 100644 index 000000000..0c6578c6c Binary files /dev/null and b/Tutorial/images/recycler_view/swiped.png differ diff --git a/Tutorial/images/recycler_view/todo_list.png b/Tutorial/images/recycler_view/todo_list.png new file mode 100644 index 000000000..b29cee6a4 Binary files /dev/null and b/Tutorial/images/recycler_view/todo_list.png differ diff --git a/Tutorial/images/scenario/login_activity.png b/Tutorial/images/scenario/login_activity.png new file mode 100644 index 000000000..b84ef1757 Binary files /dev/null and b/Tutorial/images/scenario/login_activity.png differ diff --git a/Tutorial/images/scenario/main_screen_login_button.png b/Tutorial/images/scenario/main_screen_login_button.png new file mode 100644 index 000000000..f6f0b00ba Binary files /dev/null and b/Tutorial/images/scenario/main_screen_login_button.png differ diff --git a/Tutorial/images/scenario/screen_after_login.png b/Tutorial/images/scenario/screen_after_login.png new file mode 100644 index 000000000..b7f7351be Binary files /dev/null and b/Tutorial/images/scenario/screen_after_login.png differ diff --git a/Tutorial/images/screenshot_tests_1/Initial_state_en.png b/Tutorial/images/screenshot_tests_1/Initial_state_en.png new file mode 100644 index 000000000..040e39892 Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/Initial_state_en.png differ diff --git a/Tutorial/images/screenshot_tests_1/Initial_state_fr.png b/Tutorial/images/screenshot_tests_1/Initial_state_fr.png new file mode 100644 index 000000000..f8dbbca32 Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/Initial_state_fr.png differ diff --git a/Tutorial/images/screenshot_tests_1/create_screenshot_test.png b/Tutorial/images/screenshot_tests_1/create_screenshot_test.png new file mode 100644 index 000000000..b70bb2510 Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/create_screenshot_test.png differ diff --git a/Tutorial/images/screenshot_tests_1/fr_locale.png b/Tutorial/images/screenshot_tests_1/fr_locale.png new file mode 100644 index 000000000..59fcd3b91 Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/fr_locale.png differ diff --git a/Tutorial/images/screenshot_tests_1/french.png b/Tutorial/images/screenshot_tests_1/french.png new file mode 100644 index 000000000..965c22eed Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/french.png differ diff --git a/Tutorial/images/screenshot_tests_1/initial_en.png b/Tutorial/images/screenshot_tests_1/initial_en.png new file mode 100644 index 000000000..040e39892 Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/initial_en.png differ diff --git a/Tutorial/images/screenshot_tests_1/initial_fr.png b/Tutorial/images/screenshot_tests_1/initial_fr.png new file mode 100644 index 000000000..f8dbbca32 Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/initial_fr.png differ diff --git a/Tutorial/images/screenshot_tests_1/screenshot_test.png b/Tutorial/images/screenshot_tests_1/screenshot_test.png new file mode 100644 index 000000000..fafde0c64 Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/screenshot_test.png differ diff --git a/Tutorial/images/screenshot_tests_1/success_tests.png b/Tutorial/images/screenshot_tests_1/success_tests.png new file mode 100644 index 000000000..f9956a3bc Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/success_tests.png differ diff --git a/Tutorial/images/screenshot_tests_1/todo_on_screen.png b/Tutorial/images/screenshot_tests_1/todo_on_screen.png new file mode 100644 index 000000000..9d30def3f Binary files /dev/null and b/Tutorial/images/screenshot_tests_1/todo_on_screen.png differ diff --git a/Tutorial/images/screenshot_tests_2/create_class.png b/Tutorial/images/screenshot_tests_2/create_class.png new file mode 100644 index 000000000..b6eebf742 Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/create_class.png differ diff --git a/Tutorial/images/screenshot_tests_2/example_1.png b/Tutorial/images/screenshot_tests_2/example_1.png new file mode 100644 index 000000000..ab5f4aa77 Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/example_1.png differ diff --git a/Tutorial/images/screenshot_tests_2/example_2.png b/Tutorial/images/screenshot_tests_2/example_2.png new file mode 100644 index 000000000..b26032132 Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/example_2.png differ diff --git a/Tutorial/images/screenshot_tests_2/example_3.png b/Tutorial/images/screenshot_tests_2/example_3.png new file mode 100644 index 000000000..0bf3ba1d1 Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/example_3.png differ diff --git a/Tutorial/images/screenshot_tests_2/example_4.png b/Tutorial/images/screenshot_tests_2/example_4.png new file mode 100644 index 000000000..1dbf30df4 Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/example_4.png differ diff --git a/Tutorial/images/screenshot_tests_2/example_5.png b/Tutorial/images/screenshot_tests_2/example_5.png new file mode 100644 index 000000000..46106b5e0 Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/example_5.png differ diff --git a/Tutorial/images/screenshot_tests_2/gradle.png b/Tutorial/images/screenshot_tests_2/gradle.png new file mode 100644 index 000000000..f78e8e90d Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/gradle.png differ diff --git a/Tutorial/images/screenshot_tests_2/gradle2.png b/Tutorial/images/screenshot_tests_2/gradle2.png new file mode 100644 index 000000000..a0a3c904e Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/gradle2.png differ diff --git a/Tutorial/images/screenshot_tests_2/page_object.png b/Tutorial/images/screenshot_tests_2/page_object.png new file mode 100644 index 000000000..631c30b95 Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/page_object.png differ diff --git a/Tutorial/images/screenshot_tests_2/style.png b/Tutorial/images/screenshot_tests_2/style.png new file mode 100644 index 000000000..f252d4ff6 Binary files /dev/null and b/Tutorial/images/screenshot_tests_2/style.png differ diff --git a/Tutorial/images/simple_test/First_tutorial_screen.png b/Tutorial/images/simple_test/First_tutorial_screen.png new file mode 100644 index 000000000..cab74400f Binary files /dev/null and b/Tutorial/images/simple_test/First_tutorial_screen.png differ diff --git a/Tutorial/images/simple_test/Launch_tutorial.png b/Tutorial/images/simple_test/Launch_tutorial.png new file mode 100644 index 000000000..07f08844d Binary files /dev/null and b/Tutorial/images/simple_test/Launch_tutorial.png differ diff --git a/Tutorial/images/simple_test/Layout_inspector_in_studio.png b/Tutorial/images/simple_test/Layout_inspector_in_studio.png new file mode 100644 index 000000000..d8d53ee12 Binary files /dev/null and b/Tutorial/images/simple_test/Layout_inspector_in_studio.png differ diff --git a/Tutorial/images/simple_test/Select_tutorial.png b/Tutorial/images/simple_test/Select_tutorial.png new file mode 100644 index 000000000..cd898ed00 Binary files /dev/null and b/Tutorial/images/simple_test/Select_tutorial.png differ diff --git a/Tutorial/images/simple_test/Tutorial_build_gradle.png b/Tutorial/images/simple_test/Tutorial_build_gradle.png new file mode 100644 index 000000000..10c329ad9 Binary files /dev/null and b/Tutorial/images/simple_test/Tutorial_build_gradle.png differ diff --git a/Tutorial/images/simple_test/Tutorial_main.png b/Tutorial/images/simple_test/Tutorial_main.png new file mode 100644 index 000000000..b38c5127a Binary files /dev/null and b/Tutorial/images/simple_test/Tutorial_main.png differ diff --git a/Tutorial/images/simple_test/bottom_layout_inspector.png b/Tutorial/images/simple_test/bottom_layout_inspector.png new file mode 100644 index 000000000..51b6f3ce1 Binary files /dev/null and b/Tutorial/images/simple_test/bottom_layout_inspector.png differ diff --git a/Tutorial/images/simple_test/button_id_search.png b/Tutorial/images/simple_test/button_id_search.png new file mode 100644 index 000000000..3a68a5584 Binary files /dev/null and b/Tutorial/images/simple_test/button_id_search.png differ diff --git a/Tutorial/images/simple_test/button_in_layout.png b/Tutorial/images/simple_test/button_in_layout.png new file mode 100644 index 000000000..7f6473b10 Binary files /dev/null and b/Tutorial/images/simple_test/button_in_layout.png differ diff --git a/Tutorial/images/simple_test/button_inspect.png b/Tutorial/images/simple_test/button_inspect.png new file mode 100644 index 000000000..8f23b309a Binary files /dev/null and b/Tutorial/images/simple_test/button_inspect.png differ diff --git a/Tutorial/images/simple_test/change_package.png b/Tutorial/images/simple_test/change_package.png new file mode 100644 index 000000000..107c943a2 Binary files /dev/null and b/Tutorial/images/simple_test/change_package.png differ diff --git a/Tutorial/images/simple_test/choose_process.png b/Tutorial/images/simple_test/choose_process.png new file mode 100644 index 000000000..6742613d3 Binary files /dev/null and b/Tutorial/images/simple_test/choose_process.png differ diff --git a/Tutorial/images/simple_test/create_class.png b/Tutorial/images/simple_test/create_class.png new file mode 100644 index 000000000..c7517eec9 Binary files /dev/null and b/Tutorial/images/simple_test/create_class.png differ diff --git a/Tutorial/images/simple_test/create_directory.png b/Tutorial/images/simple_test/create_directory.png new file mode 100644 index 000000000..3dde7300c Binary files /dev/null and b/Tutorial/images/simple_test/create_directory.png differ diff --git a/Tutorial/images/simple_test/create_main_screen.png b/Tutorial/images/simple_test/create_main_screen.png new file mode 100644 index 000000000..71871c05c Binary files /dev/null and b/Tutorial/images/simple_test/create_main_screen.png differ diff --git a/Tutorial/images/simple_test/create_package.png b/Tutorial/images/simple_test/create_package.png new file mode 100644 index 000000000..fec70301d Binary files /dev/null and b/Tutorial/images/simple_test/create_package.png differ diff --git a/Tutorial/images/simple_test/create_test_1.png b/Tutorial/images/simple_test/create_test_1.png new file mode 100644 index 000000000..9fc642d55 Binary files /dev/null and b/Tutorial/images/simple_test/create_test_1.png differ diff --git a/Tutorial/images/simple_test/create_test_2.png b/Tutorial/images/simple_test/create_test_2.png new file mode 100644 index 000000000..95633ddd9 Binary files /dev/null and b/Tutorial/images/simple_test/create_test_2.png differ diff --git a/Tutorial/images/simple_test/find_layout.png b/Tutorial/images/simple_test/find_layout.png new file mode 100644 index 000000000..2082d2ba2 Binary files /dev/null and b/Tutorial/images/simple_test/find_layout.png differ diff --git a/Tutorial/images/simple_test/find_string_in_layout.png b/Tutorial/images/simple_test/find_string_in_layout.png new file mode 100644 index 000000000..f61ca2c8f Binary files /dev/null and b/Tutorial/images/simple_test/find_string_in_layout.png differ diff --git a/Tutorial/images/simple_test/input_inspect.png b/Tutorial/images/simple_test/input_inspect.png new file mode 100644 index 000000000..03b77172c Binary files /dev/null and b/Tutorial/images/simple_test/input_inspect.png differ diff --git a/Tutorial/images/simple_test/kbaseview_children.png b/Tutorial/images/simple_test/kbaseview_children.png new file mode 100644 index 000000000..144a6e3b2 Binary files /dev/null and b/Tutorial/images/simple_test/kbaseview_children.png differ diff --git a/Tutorial/images/simple_test/loaded_inspector.png b/Tutorial/images/simple_test/loaded_inspector.png new file mode 100644 index 000000000..9e238777f Binary files /dev/null and b/Tutorial/images/simple_test/loaded_inspector.png differ diff --git a/Tutorial/images/simple_test/master_branch.png b/Tutorial/images/simple_test/master_branch.png new file mode 100644 index 000000000..0995a122d Binary files /dev/null and b/Tutorial/images/simple_test/master_branch.png differ diff --git a/Tutorial/images/simple_test/move_to_package.png b/Tutorial/images/simple_test/move_to_package.png new file mode 100644 index 000000000..cdd8a1e92 Binary files /dev/null and b/Tutorial/images/simple_test/move_to_package.png differ diff --git a/Tutorial/images/simple_test/name_android_test.png b/Tutorial/images/simple_test/name_android_test.png new file mode 100644 index 000000000..625549524 Binary files /dev/null and b/Tutorial/images/simple_test/name_android_test.png differ diff --git a/Tutorial/images/simple_test/needed_children.png b/Tutorial/images/simple_test/needed_children.png new file mode 100644 index 000000000..91ef3350c Binary files /dev/null and b/Tutorial/images/simple_test/needed_children.png differ diff --git a/Tutorial/images/simple_test/override.png b/Tutorial/images/simple_test/override.png new file mode 100644 index 000000000..41118ab95 Binary files /dev/null and b/Tutorial/images/simple_test/override.png differ diff --git a/Tutorial/images/simple_test/package_name_main_activity.png b/Tutorial/images/simple_test/package_name_main_activity.png new file mode 100644 index 000000000..657219e90 Binary files /dev/null and b/Tutorial/images/simple_test/package_name_main_activity.png differ diff --git a/Tutorial/images/simple_test/package_name_screen.png b/Tutorial/images/simple_test/package_name_screen.png new file mode 100644 index 000000000..f968667dd Binary files /dev/null and b/Tutorial/images/simple_test/package_name_screen.png differ diff --git a/Tutorial/images/simple_test/show_kbutton_source.png b/Tutorial/images/simple_test/show_kbutton_source.png new file mode 100644 index 000000000..e0359d18a Binary files /dev/null and b/Tutorial/images/simple_test/show_kbutton_source.png differ diff --git a/Tutorial/images/simple_test/simple_test_button.png b/Tutorial/images/simple_test/simple_test_button.png new file mode 100644 index 000000000..90c7024a8 Binary files /dev/null and b/Tutorial/images/simple_test/simple_test_button.png differ diff --git a/Tutorial/images/simple_test/string_in_values.png b/Tutorial/images/simple_test/string_in_values.png new file mode 100644 index 000000000..e1e5178ce Binary files /dev/null and b/Tutorial/images/simple_test/string_in_values.png differ diff --git a/Tutorial/images/simple_test/success_1.png b/Tutorial/images/simple_test/success_1.png new file mode 100644 index 000000000..d7f7abc45 Binary files /dev/null and b/Tutorial/images/simple_test/success_1.png differ diff --git a/Tutorial/images/simple_test/sucess_2.png b/Tutorial/images/simple_test/sucess_2.png new file mode 100644 index 000000000..5d1c0160d Binary files /dev/null and b/Tutorial/images/simple_test/sucess_2.png differ diff --git a/Tutorial/images/simple_test/switch_to_results.png b/Tutorial/images/simple_test/switch_to_results.png new file mode 100644 index 000000000..b04581b6d Binary files /dev/null and b/Tutorial/images/simple_test/switch_to_results.png differ diff --git a/Tutorial/images/simple_test/test_failed_1.png b/Tutorial/images/simple_test/test_failed_1.png new file mode 100644 index 000000000..8250d2543 Binary files /dev/null and b/Tutorial/images/simple_test/test_failed_1.png differ diff --git a/Tutorial/images/simple_test/title_inspect.png b/Tutorial/images/simple_test/title_inspect.png new file mode 100644 index 000000000..95ca5e262 Binary files /dev/null and b/Tutorial/images/simple_test/title_inspect.png differ diff --git a/Tutorial/images/steps/clear_logcat.png b/Tutorial/images/steps/clear_logcat.png new file mode 100644 index 000000000..93660de8c Binary files /dev/null and b/Tutorial/images/steps/clear_logcat.png differ diff --git a/Tutorial/images/steps/create_filter.png b/Tutorial/images/steps/create_filter.png new file mode 100644 index 000000000..ee8bacb42 Binary files /dev/null and b/Tutorial/images/steps/create_filter.png differ diff --git a/Tutorial/images/steps/edit_configuration.png b/Tutorial/images/steps/edit_configuration.png new file mode 100644 index 000000000..4fbc0b43f Binary files /dev/null and b/Tutorial/images/steps/edit_configuration.png differ diff --git a/Tutorial/images/steps/log_step_1.png b/Tutorial/images/steps/log_step_1.png new file mode 100644 index 000000000..e893e14fc Binary files /dev/null and b/Tutorial/images/steps/log_step_1.png differ diff --git a/Tutorial/images/steps/log_step_2.png b/Tutorial/images/steps/log_step_2.png new file mode 100644 index 000000000..14cb2a69e Binary files /dev/null and b/Tutorial/images/steps/log_step_2.png differ diff --git a/Tutorial/images/steps/log_step_2_failed.png b/Tutorial/images/steps/log_step_2_failed.png new file mode 100644 index 000000000..66143511e Binary files /dev/null and b/Tutorial/images/steps/log_step_2_failed.png differ diff --git a/Tutorial/images/steps/log_step_3.png b/Tutorial/images/steps/log_step_3.png new file mode 100644 index 000000000..36e7a2a24 Binary files /dev/null and b/Tutorial/images/steps/log_step_3.png differ diff --git a/Tutorial/images/steps/log_with_steps.png b/Tutorial/images/steps/log_with_steps.png new file mode 100644 index 000000000..a9bf7f269 Binary files /dev/null and b/Tutorial/images/steps/log_with_steps.png differ diff --git a/Tutorial/images/steps/logcat.png b/Tutorial/images/steps/logcat.png new file mode 100644 index 000000000..790c53d9c Binary files /dev/null and b/Tutorial/images/steps/logcat.png differ diff --git a/Tutorial/images/steps/test_failed_with_steps.png b/Tutorial/images/steps/test_failed_with_steps.png new file mode 100644 index 000000000..b81c51598 Binary files /dev/null and b/Tutorial/images/steps/test_failed_with_steps.png differ diff --git a/Tutorial/images/uiautomator/da_1_settings.png b/Tutorial/images/uiautomator/da_1_settings.png new file mode 100644 index 000000000..127cb5d55 Binary files /dev/null and b/Tutorial/images/uiautomator/da_1_settings.png differ diff --git a/Tutorial/images/uiautomator/da_2_settings.png b/Tutorial/images/uiautomator/da_2_settings.png new file mode 100644 index 000000000..425d630d4 Binary files /dev/null and b/Tutorial/images/uiautomator/da_2_settings.png differ diff --git a/Tutorial/images/uiautomator/da_3_settings.png b/Tutorial/images/uiautomator/da_3_settings.png new file mode 100644 index 000000000..695657f00 Binary files /dev/null and b/Tutorial/images/uiautomator/da_3_settings.png differ diff --git a/Tutorial/images/uiautomator/da_4_settings.png b/Tutorial/images/uiautomator/da_4_settings.png new file mode 100644 index 000000000..069b0b7d7 Binary files /dev/null and b/Tutorial/images/uiautomator/da_4_settings.png differ diff --git a/Tutorial/images/uiautomator/da_5_settings.png b/Tutorial/images/uiautomator/da_5_settings.png new file mode 100644 index 000000000..6cd5b307d Binary files /dev/null and b/Tutorial/images/uiautomator/da_5_settings.png differ diff --git a/Tutorial/images/uiautomator/da_6_settings.png b/Tutorial/images/uiautomator/da_6_settings.png new file mode 100644 index 000000000..8f9ab3417 Binary files /dev/null and b/Tutorial/images/uiautomator/da_6_settings.png differ diff --git a/Tutorial/images/uiautomator/da_gplay_1.png b/Tutorial/images/uiautomator/da_gplay_1.png new file mode 100644 index 000000000..78ba8905f Binary files /dev/null and b/Tutorial/images/uiautomator/da_gplay_1.png differ diff --git a/Tutorial/images/uiautomator/da_gplay_2.png b/Tutorial/images/uiautomator/da_gplay_2.png new file mode 100644 index 000000000..9d420ec3b Binary files /dev/null and b/Tutorial/images/uiautomator/da_gplay_2.png differ diff --git a/Tutorial/images/uiautomator/da_gplay_3.png b/Tutorial/images/uiautomator/da_gplay_3.png new file mode 100644 index 000000000..f66267022 Binary files /dev/null and b/Tutorial/images/uiautomator/da_gplay_3.png differ diff --git a/Tutorial/images/uiautomator/dump_1.png b/Tutorial/images/uiautomator/dump_1.png new file mode 100644 index 000000000..fc4a4f3db Binary files /dev/null and b/Tutorial/images/uiautomator/dump_1.png differ diff --git a/Tutorial/images/uiautomator/dump_2.png b/Tutorial/images/uiautomator/dump_2.png new file mode 100644 index 000000000..0e7a6b8d0 Binary files /dev/null and b/Tutorial/images/uiautomator/dump_2.png differ diff --git a/Tutorial/images/uiautomator/dump_3.png b/Tutorial/images/uiautomator/dump_3.png new file mode 100644 index 000000000..43753705d Binary files /dev/null and b/Tutorial/images/uiautomator/dump_3.png differ diff --git a/Tutorial/images/uiautomator/dump_4.png b/Tutorial/images/uiautomator/dump_4.png new file mode 100644 index 000000000..be4e9ef19 Binary files /dev/null and b/Tutorial/images/uiautomator/dump_4.png differ diff --git a/Tutorial/images/uiautomator/dump_5.png b/Tutorial/images/uiautomator/dump_5.png new file mode 100644 index 000000000..c7862a998 Binary files /dev/null and b/Tutorial/images/uiautomator/dump_5.png differ diff --git a/Tutorial/images/uiautomator/dump_6.png b/Tutorial/images/uiautomator/dump_6.png new file mode 100644 index 000000000..b2ab44ed4 Binary files /dev/null and b/Tutorial/images/uiautomator/dump_6.png differ diff --git a/Tutorial/images/uiautomator/dump_7.png b/Tutorial/images/uiautomator/dump_7.png new file mode 100644 index 000000000..893bde5f4 Binary files /dev/null and b/Tutorial/images/uiautomator/dump_7.png differ diff --git a/Tutorial/images/uiautomator/google_play_unauth.png b/Tutorial/images/uiautomator/google_play_unauth.png new file mode 100644 index 000000000..94394250a Binary files /dev/null and b/Tutorial/images/uiautomator/google_play_unauth.png differ diff --git a/Tutorial/images/uiautomator/matchers.png b/Tutorial/images/uiautomator/matchers.png new file mode 100644 index 000000000..d837cf072 Binary files /dev/null and b/Tutorial/images/uiautomator/matchers.png differ diff --git a/Tutorial/images/uiautomator/notification.png b/Tutorial/images/uiautomator/notification.png new file mode 100644 index 000000000..95fcb06a5 Binary files /dev/null and b/Tutorial/images/uiautomator/notification.png differ diff --git a/Tutorial/images/uiautomator/notification_activity_btn.png b/Tutorial/images/uiautomator/notification_activity_btn.png new file mode 100644 index 000000000..f10de9d54 Binary files /dev/null and b/Tutorial/images/uiautomator/notification_activity_btn.png differ diff --git a/Tutorial/images/uiautomator/ui_button.png b/Tutorial/images/uiautomator/ui_button.png new file mode 100644 index 000000000..e280d5935 Binary files /dev/null and b/Tutorial/images/uiautomator/ui_button.png differ diff --git a/Tutorial/images/uiautomator/uiautomator_button.png b/Tutorial/images/uiautomator/uiautomator_button.png new file mode 100644 index 000000000..42295cbd9 Binary files /dev/null and b/Tutorial/images/uiautomator/uiautomator_button.png differ diff --git a/Tutorial/images/uiautomator/uiautomator_notification.png b/Tutorial/images/uiautomator/uiautomator_notification.png new file mode 100644 index 000000000..cf34c68ff Binary files /dev/null and b/Tutorial/images/uiautomator/uiautomator_notification.png differ diff --git a/Tutorial/images/uiautomator/uiautomator_package.png b/Tutorial/images/uiautomator/uiautomator_package.png new file mode 100644 index 000000000..d3e4e93d9 Binary files /dev/null and b/Tutorial/images/uiautomator/uiautomator_package.png differ diff --git a/Tutorial/images/uiautomator/uiautomatorviewer_1.png b/Tutorial/images/uiautomator/uiautomatorviewer_1.png new file mode 100644 index 000000000..aab4d6c54 Binary files /dev/null and b/Tutorial/images/uiautomator/uiautomatorviewer_1.png differ diff --git a/Tutorial/images/uiautomator/uiautomatorviewer_2.png b/Tutorial/images/uiautomator/uiautomatorviewer_2.png new file mode 100644 index 000000000..22193d087 Binary files /dev/null and b/Tutorial/images/uiautomator/uiautomatorviewer_2.png differ diff --git a/Tutorial/images/wifi_test/available_methods.png b/Tutorial/images/wifi_test/available_methods.png new file mode 100644 index 000000000..3bf2be02d Binary files /dev/null and b/Tutorial/images/wifi_test/available_methods.png differ diff --git a/Tutorial/images/wifi_test/first_launch_1.png b/Tutorial/images/wifi_test/first_launch_1.png new file mode 100644 index 000000000..bfadc03b1 Binary files /dev/null and b/Tutorial/images/wifi_test/first_launch_1.png differ diff --git a/Tutorial/images/wifi_test/first_launch_2.png b/Tutorial/images/wifi_test/first_launch_2.png new file mode 100644 index 000000000..82d833b9d Binary files /dev/null and b/Tutorial/images/wifi_test/first_launch_2.png differ diff --git a/Tutorial/images/wifi_test/internet_availability_button.png b/Tutorial/images/wifi_test/internet_availability_button.png new file mode 100644 index 000000000..4dc1a5c8a Binary files /dev/null and b/Tutorial/images/wifi_test/internet_availability_button.png differ diff --git a/Tutorial/images/wifi_test/turn_off_wifi.png b/Tutorial/images/wifi_test/turn_off_wifi.png new file mode 100644 index 000000000..6a0a297dc Binary files /dev/null and b/Tutorial/images/wifi_test/turn_off_wifi.png differ diff --git a/Tutorial/images/wifi_test/wifi_disabled.png b/Tutorial/images/wifi_test/wifi_disabled.png new file mode 100644 index 000000000..6565ff88c Binary files /dev/null and b/Tutorial/images/wifi_test/wifi_disabled.png differ diff --git a/Tutorial/images/wifi_test/wifi_disabled_portrait.png b/Tutorial/images/wifi_test/wifi_disabled_portrait.png new file mode 100644 index 000000000..16d7c7393 Binary files /dev/null and b/Tutorial/images/wifi_test/wifi_disabled_portrait.png differ diff --git a/Tutorial/images/wifi_test/wifi_enabled.png b/Tutorial/images/wifi_test/wifi_enabled.png new file mode 100644 index 000000000..b4aeddb05 Binary files /dev/null and b/Tutorial/images/wifi_test/wifi_enabled.png differ diff --git a/Tutorial/index.html b/Tutorial/index.html new file mode 100644 index 000000000..06589cd39 --- /dev/null +++ b/Tutorial/index.html @@ -0,0 +1,1491 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 1. Introduction - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + +

Introduction

+

Hi everyone! +
If you're here, it means you're interested in Android autotests. Kaspresso is a great solution that can help you. You can find more information about our framework here. +
The Kaspresso team prepared Tutorial in codelabs format. This Tutorial is designed to help you get started with Kaspresso and familiarize yourself with its main features.

+

Tutorial structure

+

The Tutorial is divided into steps (lessons). Each lesson begins with a brief overview and ends with summary and conclusions.

+

How to study this Tutorial?

+

We strive to make the lessons independent from each other, but this is not always possible. For a better understanding of Kaspresso, we recommend starting with the first lesson and moving sequentially to the next. +
The codelab format assumes that you will combine theory and practice, repeating the instructions from the lessons step by step. In the Kaspresso project, in the 'tutorial' folder, there is an example of the application code for which tests will be written. The first lesson will tell you how to download it. In the tutorial_results branch, you can see the final implementation of all tutorial tests.

+

What do you need to know to complete the Tutorial?

+

We are not trying to teach you autotests from scratch. At the same time, we do not set any restrictions on knowledge and experience for passing the tutorial and try to keep the story in such a way that it is understandable to beginners in autotests and Android. It is almost impossible to talk about Kaspresso without terms from the Java and Kotlin programming languages, the Espresso, Kakao, UiAutomator and other frameworks, the Android operating system and testing itself as an IT area. Nevertheless, the main focus is on the explanation of Kaspresso itself, and in all places where various terms are mentioned, we share links to official sources for detailed information and better understanding.

+

Feedback

+

If you find a typo, error or inaccuracy in the material, want to suggest an improvement or add new lessons to the Tutorial, you can create an Issue in the Kaspresso project or open a Pull request (materials from the Tutorial are in the public domain in the docs folder). +
If the Tutorial did not answer your question, you can search the Wiki section or the Kaspresso in articles and Kaspresso in video. +
You can also join our Telegram channels ru and en and ask your question there.

+

Give thanks

+

If you like our framework, you can give our project a star on Github.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Espresso_as_the_basis/index.html b/Wiki/Espresso_as_the_basis/index.html new file mode 100644 index 000000000..0c457c240 --- /dev/null +++ b/Wiki/Espresso_as_the_basis/index.html @@ -0,0 +1,1567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Espresso as the basis - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Espresso as the basis

+

Kaspresso is based on Google testing framework Espresso (if you're not familiar with Espresso, check out the official docs) +
Espresso allows you to work with the elements of your application as a white box (white box testing). You can find the desired element on the screen using matchers, perform different actions or checks.

+

Espresso is not enough

+

This framework has a lot of drawbacks and not all things in Android autotesting can be done with Espresso alone.

+

What do we want:

+
    +
  1. Good readability. Espresso has a problem with this because of the huge hierarchy of matchers. When we have a lot of matches, the code becomes difficult to read. Poor readability means difficult to maintain
  2. +
  3. Hight stability. Espresso does not work well with interfaces whose elements are displayed asynchronously. You can configure Idling, but that still won't solve all problems.
  4. +
  5. Logging. After completing the test with Espresso, you do not have a step-by-step workflow sequence of actions.
  6. +
  7. Screenshots. We also want to have some screenshots for the test report.
  8. +
  9. Working with Android OS. In some cases, we need to interact with the device. In this case you need UiAutomator (as a variant).
  10. +
  11. Сode architecture. We want to have a clean code architecture in our tests, the ability to reuse code, move some blocks in abstractions. One code style for all developers.
  12. +
+

How does Kaspresso solve all these problems?

+

Readability

+

Kaspresso is based on Kakao - Android framework for UI autotests. It is also based on Espresso. Kakao provides a simple Kotlin DSL. This makes the tests more readable. You no longer need to put long constructors with matchers for finding elements on the screen in the code of your test. The result of calling the onView() Espresso method is cached. You can then get the required view as a property. +
Kakao also provides an implementation of Page object pattern with a Screen object. You can describe all the interface elements that your test will interact with in one place (in one Screen object).

+

Stability

+

Kaspresso has wrapped some Espresso calls into a more stable implementation. For example you can find flakySafely() method in the Kaspresso.

+

Logging

+

Kaspresso has wrapped some Espresso calls not only for higher stability. We have also implemented an interceptor that prints more logs.

+

Working with Android OS

+

We have created the Device interface as a facade for all devices to work with. UiAutomator can only help you in some cases, but more often you need the ability to execute various commands (adb, shell). For example, with the adb emu command, you can emulate various actions or events. +
Espresso tests are run directly on the android device, so we need some kind of external server to send the commands. In Kaspresso you can use AdbServer.

+

Code architecture

+

Having described above implementations of Page object pattern, you can make your code in your test files more readable, maintainable, reusable, and understandable. Kaspresso also provides various methods and abstractions to improve the architecture (such as step, Scenario, test sections and more).

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Executing_adb_commands/index.html b/Wiki/Executing_adb_commands/index.html new file mode 100644 index 000000000..f88138a34 --- /dev/null +++ b/Wiki/Executing_adb_commands/index.html @@ -0,0 +1,1723 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Executing adb commands - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Executing adb commands

+

Description

+

As you remember from the previous part devoted to Device interface, Device interface contains the following things under the hood:

+
    +
  • Espresso
  • +
  • UI Automator
  • +
  • ADB
  • +
+ +

An attentive reader could notice that ADB is not available in Espresso tests. But using some other frameworks, like Appium, you can execute ADB commands. So we decided to add this important functionality too.
+We've developed a special Autotest's AdbServer to compensate lack of this feature. +The main idea of the tool is similar to the idea in Appium. We just built a simple client-server system which contains two parts:

+
    +
  • Device that starts up a test acts as client
  • +
  • Desktop sends ADB commands to be executed on the device. + Also, the system uses a port forwarding to be able to organize a socket tunnel between Device and Desktop through any kind of connection (Wi-Fi, Bluetooth, USB and etc.).
  • +
+

Usage

+

The algorithm how to use Autotest AdbServer:

+
    +
  1. Run the Desktop part on your work station.
    + Execute the following command: java -jar <path/to/kaspresso>/artifacts/adbserver-desktop.jar in the terminal
  2. +
  3. Run the Device part.
    + Build and start adbserver-sample module. You should see the following screen: +
  4. +
+

For example, type shell input text abc in the app's EditText and click Execute button. As result you will get shell input text abcabc +in the EditText because ADB command has been executed and abc symbols has been added into the focused EditText.
+You can notice that the app uses AdbTerminal class to execute ADB commands.

+

Usage in Kaspresso

+

In Kaspresso, we wrap AdbTerminal into a special interface AdbServer. +AdbServer's instance is available in BaseTestContext scope and BaseTestCase with adbServer property:
+There're two types of AbdServer methods signatures:

+

fun perform(vararg commands: String): List<String> and perform(command: String, arguments: List<String>): String with an important difference. +First signature accept one or more commands and execute them one by one. Example below: +

@Test
+fun test() =
+    run {
+        step("Open Simple Screen") {
+            activityTestRule.launchActivity(null)
+ ======>    adbServer.performShell("input text 1")   <======
+
+            MainScreen {
+                simpleButton {
+                    isVisible()
+                    click()
+                }
+            }
+        }
+        // ....
+}
+
+This method passes each command to the java runtime which tokenizes it by whitespaces. It could be not ideal. It can't be used for the commands with +the whitespaces in their arguments (e.g. adb pull "/sdcard/Documents/path_with whitespace to/file.txt") and it doesn't allow piping (e.g. cat $BUILD_DIR/report.txt | grep filte > filtered.txt). +That's why there's a second AdbServer methods signature type.

+

It executes a single command and allows you to pass a list of arguments. It doesn't tokenize your command or arguments, they are used "as is". +This allows you to use piping and complex arguments +See example below (yes, a command could be an argument for another command): +

@Test
+fun test() = before{
+      adbServer.performCmd("bash", listOf("-c", "adb shell dumpsys deviceidle | grep mForceIdle"))
+   }.after {
+   }.run {
+       // ...
+   }
+

+

Also, don't forget to grant necessary permission: +

<uses-permission android:name="android.permission.INTERNET" />
+

+

Options and Logging

+

Desktop part

+

You can also use a few special flags when he starts adbserver-desktop.jar.
+For example, java -jar adbserver-desktop.jar -e emulator-5554,emulator-5556 -p 5041 -l VERBOSE.
+Flags:

+
    +
  • e, --emulators - the list of emulators that can be captured by adbserver-desktop.jar (by default, adbserver-desktop.jar captures all available emulators)
  • +
  • p, --port - the adb server port number (the default value is 5037)
  • +
  • l, --logs - what type of logs show (the default value is INFO).
  • +
  • a, --adb_path - path to custom adb instance (by default, adbserver-desktop.jar uses adb from environment). +For more information, you can run java -jar adbserver-desktop.jar --help
  • +
+

Consider available types of logs: +1. ERROR
+ You will see only error messages in the output. For example, +

ERROR 10/09/2020 11:37:19.893  desktop=Desktop-25920 device=emulator-5554   message: Incorrect type of the message...
+
+Take a look at the log format. You can see the type of a message, date and time, the host name and the emulator which executes the command, and the message.

+
    +
  1. +

    WARN
    + Prints error and warning messages.

    +
  2. +
  3. +

    INFO
    + Default value, provides all the base events. For example, +

    INFO 10/09/2020 11:37:04.822  desktop=Desktop-25920    message: Desktop started with arguments: emulators=[], adbServerPort=null
    +INFO 10/09/2020 11:37:19.859  desktop=Desktop-25920    message: New device has been found: emulator-5554. Initialize connection to the device...
    +INFO 10/09/2020 11:37:19.892  desktop=Desktop-25920 device=emulator-5554   message: The connection establishment to device started
    +INFO 10/09/2020 11:37:19.893  desktop=Desktop-25920 device=emulator-5554   message: WatchdogThread is started from Desktop to Device
    +INFO 10/09/2020 11:37:19.893  desktop=Desktop-25920 device=emulator-5554   message: Desktop tries to connect to the Device.
    + It may take time because the device can be not ready. Possible reason: a kaspresso test has not been started
    +INFO 10/09/2020 11:37:20.185  desktop=Desktop-25920 device=emulator-5554   message: The attempt to connect to Device was success
    +INFO 10/09/2020 11:44:47.810  desktop=Desktop-25920 device=emulator-5554   message: The received command to execute: AdbCommand(body=shell input text abc)
    +INFO 10/09/2020 11:44:49.115  desktop=Desktop-25920 device=emulator-5554   message: The executed command: AdbCommand(body=shell input text abc). The result: CommandResult(status=SUCCESS, description=exitCode=0, message=, serviceInfo=The command was executed on desktop=Desktop-25920)
    +
    +Also, the Desktop prints an emulator name, where the concrete command has been executed (this information is available on the Desktop and on the Device). +It could be very useful in debugging. Take a look at the field serviceInfo at the end: +
    INFO 10/09/2020 11:44:49.115  desktop=Desktop-25920 device=emulator-5554   message: The executed command: AdbCommand(body=shell input text abc). The result: CommandResult(status=SUCCESS, description=exitCode=0, message=, serviceInfo=The command was executed on desktop=Desktop-25920)
    +

    +
  4. +
  5. +

    VERBOSE
    + There are cases when you might to debug Desktop part of AdbServer. That's why there is a special very detailed format — VERBOSE.
    + Have a glance at logs reflecting similar events presented above (initialization, device connection and execution of a command): +

    INFO 10/09/2020 11:48:16.850  desktop=Desktop-27398  tag=MainKt  method=main  message: Desktop started with arguments: emulators=[], adbServerPort=null
    +DEBUG 10/09/2020 11:48:16.853  desktop=Desktop-27398  tag=Desktop  method=startDevicesObserving  message: start
    +INFO 10/09/2020 11:48:16.913  desktop=Desktop-27398  tag=Desktop  method=startDevicesObserving  message: New device has been found: emulator-5554. Initialize connection to the device...
    +DEBUG 10/09/2020 11:48:16.918  desktop=Desktop-27398 device=emulator-5554 tag=DesktopDeviceSocketConnectionForwardImpl  method=getDesktopSocketLoad  message: calculated desktop client port=21234
    +DEBUG 10/09/2020 11:48:16.918  desktop=Desktop-27398 device=emulator-5554 tag=DesktopDeviceSocketConnectionForwardImpl  method=forwardPorts  message: fromPort=21234, toPort=8500 started
    +DEBUG 10/09/2020 11:48:16.919  desktop=Desktop-27398 device=emulator-5554 tag=CommandExecutorImpl  method=execute  message: The created adbCommand=adb -s emulator-5554 forward tcp:21234 tcp:8500
    +DEBUG 10/09/2020 11:48:16.925  desktop=Desktop-27398 device=emulator-5554 tag=DesktopDeviceSocketConnectionForwardImpl  method=forwardPorts  message: fromPort=21234, toPort=8500) finished with result=CommandResult(status=SUCCESS, description=exitCode=0, message=21234
    +, serviceInfo=The command was executed on desktop=Desktop-27398)
    +DEBUG 10/09/2020 11:48:16.925  desktop=Desktop-27398 device=emulator-5554 tag=DesktopDeviceSocketConnectionForwardImpl  method=getDesktopSocketLoad  message: desktop client port=21234 is forwarding with device server port=8500
    +INFO 10/09/2020 11:48:16.927  desktop=Desktop-27398 device=emulator-5554 tag=DeviceMirror  method=startConnectionToDevice  message: The connection establishment to device started
    +INFO 10/09/2020 11:48:16.928  desktop=Desktop-27398 device=emulator-5554 tag=DeviceMirror$WatchdogThread  method=run  message: WatchdogThread is started from Desktop to Device
    +DEBUG 10/09/2020 11:48:16.928  desktop=Desktop-27398 device=emulator-5554 tag=DeviceMirror$WatchdogThread  method=run  message: The attempt to connect to Device. It may take time because the device can be not ready (for example, a kaspresso test was not started).
    +INFO 10/09/2020 11:48:16.928  desktop=Desktop-27398 device=emulator-5554 tag=DeviceMirror$WatchdogThread  method=run  message: Desktop tries to connect to the Device.
    + It may take time because the device can be not ready. Possible reason: a kaspresso test has not been started
    +DEBUG 10/09/2020 11:48:16.929  desktop=Desktop-27398 device=emulator-5554 tag=ConnectionServerImplBySocket  method=tryConnect  message: Start the process
    +DEBUG 10/09/2020 11:48:16.929  desktop=Desktop-27398 device=emulator-5554 tag=ConnectionMaker  method=connect  message: Start a connection establishment. The current state=DISCONNECTED
    +DEBUG 10/09/2020 11:48:16.929  desktop=Desktop-27398 device=emulator-5554 tag=ConnectionMaker  method=connect  message: The current state=CONNECTING
    +DEBUG 10/09/2020 11:48:16.930  desktop=Desktop-27398 device=emulator-5554 tag=DesktopDeviceSocketConnectionForwardImpl$getDesktopSocketLoad$1  method=invoke  message: started with ip=127.0.0.1, port=21234
    +DEBUG 10/09/2020 11:48:16.938  desktop=Desktop-27398 device=emulator-5554 tag=DesktopDeviceSocketConnectionForwardImpl$getDesktopSocketLoad$1  method=invoke  message: completed with ip=127.0.0.1, port=21234
    +DEBUG 10/09/2020 11:48:16.941  desktop=Desktop-27398 device=emulator-5554 tag=SocketMessagesTransferring  method=prepareListening  message: Start
    +DEBUG 10/09/2020 11:48:16.948  desktop=Desktop-27398 device=emulator-5554 tag=SocketMessagesTransferring  method=prepareListening  message: IO Streams were created
    +DEBUG 10/09/2020 11:48:16.948  desktop=Desktop-27398 device=emulator-5554 tag=ConnectionMaker  method=connect  message: The connection is established. The current state=CONNECTED
    +DEBUG 10/09/2020 11:48:16.948  desktop=Desktop-27398 device=emulator-5554 tag=ConnectionServerImplBySocket$tryConnect$2  method=invoke  message: The connection is ready. Start messages listening
    +DEBUG 10/09/2020 11:48:16.949  desktop=Desktop-27398 device=emulator-5554 tag=SocketMessagesTransferring  method=startListening  message: Started
    +INFO 10/09/2020 11:48:16.949  desktop=Desktop-27398 device=emulator-5554 tag=DeviceMirror$WatchdogThread  method=run  message: The attempt to connect to Device was success
    +DEBUG 10/09/2020 11:48:16.949  desktop=Desktop-27398 device=emulator-5554 tag=SocketMessagesTransferring$MessagesListeningThread  method=run  message: Start listening
    +DEBUG 10/09/2020 11:48:24.132  desktop=Desktop-27398 device=emulator-5554 tag=SocketMessagesTransferring  method=peekNextMessage  message: The message=TaskMessage(command=AdbCommand(body=shell input text abc))
    +INFO 10/09/2020 11:48:24.132  desktop=Desktop-27398 device=emulator-5554 tag=DeviceMirror$Companion$create$connectionServerLifecycle$1  method=onReceivedTask  message: The received command to execute: AdbCommand(body=shell input text abc)
    +DEBUG 10/09/2020 11:48:24.132  desktop=Desktop-27398 device=emulator-5554 tag=ConnectionServerImplBySocket$handleMessages$1  method=invoke  message: Received taskMessage=TaskMessage(command=AdbCommand(body=shell input text abc))
    +DEBUG 10/09/2020 11:48:24.133  desktop=Desktop-27398 device=emulator-5554 tag=CommandExecutorImpl  method=execute  message: The created adbCommand=adb -s emulator-5554 shell input text abc
    +INFO 10/09/2020 11:48:24.389  desktop=Desktop-27398 device=emulator-5554 tag=DeviceMirror$Companion$create$connectionServerLifecycle$1  method=onExecutedTask  message: The executed command: AdbCommand(body=shell input text abc). The result: CommandResult(status=SUCCESS, description=exitCode=0, message=, serviceInfo=The command was executed on desktop=Desktop-27398)
    +DEBUG 10/09/2020 11:48:24.389  desktop=Desktop-27398 device=emulator-5554 tag=ConnectionServerImplBySocket$handleMessages$1$1  method=run  message: Result of taskMessage=TaskMessage(command=AdbCommand(body=shell input text abc)) => result=CommandResult(status=SUCCESS, description=exitCode=0, message=, serviceInfo=The command was executed on desktop=Desktop-27398)
    +DEBUG 10/09/2020 11:48:24.389  desktop=Desktop-27398 device=emulator-5554 tag=SocketMessagesTransferring  method=sendMessage  message: Input sendModel=ResultMessage(command=AdbCommand(body=shell input text abc), data=CommandResult(status=SUCCESS, description=exitCode=0, message=, serviceInfo=The command was executed on desktop=Desktop-27398))
    +
    +Pay attention that the log row also contains two additional fields: tag and method. Both fields are autogenerated using Throwable().stacktrace method.

    +
  6. +
  7. +

    DEBUG
    + Unlike a VERBOSE type, DEBUG packs repeating pieces of logs. For example, +

    DEBUG 10/09/2020 12:11:37.006  desktop=Desktop-30548 device=emulator-5554 tag=DeviceMirror$WatchdogThread  method=run  message: The attempt to connect to Device. It may take time because the device can be not ready (for example, a kaspresso test was not started).
    +DEBUG 10/09/2020 12:11:44.063  desktop=Desktop-30548 device=emulator-5554 tag=ServiceInfo  method=Start  message: ////////////////////////////////////////FRAGMENT IS REPEATED 7 TIMES////////////////////////////////////////
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=ConnectionServerImplBySocket  method=tryConnect  message: Start the process
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=ConnectionMaker  method=connect  message: Start a connection establishment. The current state=DISCONNECTED
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=ConnectionMaker  method=connect  message: The current state=CONNECTING
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=DesktopDeviceSocketConnectionForwardImpl$getDesktopSocketLoad$1  method=invoke  message: started with ip=127.0.0.1, port=37110
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=DesktopDeviceSocketConnectionForwardImpl$getDesktopSocketLoad$1  method=invoke  message: completed with ip=127.0.0.1, port=37110
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=SocketMessagesTransferring  method=prepareListening  message: Start
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=ConnectionMaker  method=connect  message: The connection establishment process failed. The current state=DISCONNECTED
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=ConnectionServerImplBySocket$tryConnect$3  method=invoke  message: The connection establishment attempt failed. The most possible reason is the opposite socket is not ready yet
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=DeviceMirror$WatchdogThread  method=run  message: The attempt to connect to Device. It may take time because the device can be not ready (for example, a kaspresso test was not started).
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=ServiceInfo  method=End  message: ////////////////////////////////////////////////////////////////////////////////////////////////////
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=ConnectionServerImplBySocket  method=tryConnect  message: Start the process
    +DEBUG 10/09/2020 12:11:44.064  desktop=Desktop-30548 device=emulator-5554 tag=ConnectionMaker  method=connect  message: Start a connection establishment. The current state=DISCONNECTED
    +

    +
  8. +
+

Device part

+

In Kaspresso, the AdbServer interface has a default implementation AdbServerImpl. This implementation sets WARN log level for AdbServer. +So, you can see such logs in LogCat:
+

2020-09-10 12:24:27.240 10349-10378/com.kaspersky.kaspressample I/KASPRESSO: ___________________________________________________________________________
+2020-09-10 12:24:27.240 10349-10378/com.kaspersky.kaspressample I/KASPRESSO: TEST STEP: "1. Disable network" in DeviceNetworkSampleTest
+2020-09-10 12:24:27.240 10349-10378/com.kaspersky.kaspressample I/KASPRESSO: AdbServer. The command to execute=su 0 svc data disable
+2020-09-10 12:24:27.240 10349-10378/com.kaspersky.kaspressample W/KASPRESSO_ADBSERVER: Something went wrong (fake message)
+
+All the logs are printed with KASPRESSO_ADBSERVER tag with WARN log level.
+If you want to debug the Device part of Autotest AdbServer (the device part), you can set VERBOSE log level: +
class DeviceNetworkSampleTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple {
+        libLogger = UiTestLoggerImpl(Kaspresso.DEFAULT_LIB_LOGGER_TAG)
+        adbServer = AdbServerImpl(LogLevel.VERBOSE, libLogger)
+    }
+) {...}
+
+Now the logs looks like: +
2020-09-10 12:24:27.240 10349-10378/com.kaspersky.kaspressample I/KASPRESSO: TEST STEP: "1. Disable network" in DeviceNetworkSampleTest
+2020-09-10 12:24:27.240 10349-10378/com.kaspersky.kaspressample I/KASPRESSO: AdbServer. The command to execute=su 0 svc data disable
+2020-09-10 12:24:27.240 10349-10378/com.kaspersky.kaspressample I/KASPRESSO_ADBSERVER: Start to execute the command=AdbCommand(body=shell su 0 svc data disable)
+2020-09-10 12:24:27.240 10349-10378/com.kaspersky.kaspressample D/KASPRESSO_ADBSERVER: class=ConnectionClientImplBySocket method=executeCommand message: Started command=AdbCommand(body=shell su 0 svc data disable)
+2020-09-10 12:24:27.241 10349-10378/com.kaspersky.kaspressample D/KASPRESSO_ADBSERVER: class=SocketMessagesTransferring method=sendMessage message: Input sendModel=TaskMessage(command=AdbCommand(body=shell su 0 svc data disable))
+2020-09-10 12:24:27.427 10349-10406/com.kaspersky.kaspressample D/KASPRESSO_ADBSERVER: class=SocketMessagesTransferring method=peekNextMessage message: The message=ResultMessage(command=AdbCommand(body=shell su 0 svc data disable), data=CommandResult(status=SUCCESS, description=exitCode=0, message=, serviceInfo=The command was executed on desktop=Desktop-30548))
+2020-09-10 12:24:27.427 10349-10406/com.kaspersky.kaspressample D/KASPRESSO_ADBSERVER: class=ConnectionClientImplBySocket$handleMessages$1 method=invoke message: Received resultMessage=ResultMessage(command=AdbCommand(body=shell su 0 svc data disable), data=CommandResult(status=SUCCESS, description=exitCode=0, message=, serviceInfo=The command was executed on desktop=Desktop-30548))
+2020-09-10 12:24:27.427 10349-10378/com.kaspersky.kaspressample D/KASPRESSO_ADBSERVER: class=ConnectionClientImplBySocket method=executeCommand message: Command=AdbCommand(body=shell su 0 svc data disable) completed with commandResult=CommandResult(status=SUCCESS, description=exitCode=0, message=, serviceInfo=The command was executed on desktop=Desktop-30548)
+2020-09-10 12:24:27.427 10349-10378/com.kaspersky.kaspressample I/KASPRESSO_ADBSERVER: The result of command=AdbCommand(body=shell su 0 svc data disable) => CommandResult(status=SUCCESS, description=exitCode=0, message=, serviceInfo=The command was executed on desktop=Desktop-30548)
+

+

Development

+

The source code of AdbServer is available in adb-server module.
+If you want to build adbserver-desktop.jar manually, just execute ./gradlew :adb-server:adbserver-desktop:assemble.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Images/Matchers_actions_assertions/Espresso_cheat_sheet.png b/Wiki/Images/Matchers_actions_assertions/Espresso_cheat_sheet.png new file mode 100644 index 000000000..8dea00033 Binary files /dev/null and b/Wiki/Images/Matchers_actions_assertions/Espresso_cheat_sheet.png differ diff --git a/Wiki/Jetpack_Compose/index.html b/Wiki/Jetpack_Compose/index.html new file mode 100644 index 000000000..b205d863f --- /dev/null +++ b/Wiki/Jetpack_Compose/index.html @@ -0,0 +1,1763 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Compose support in Kaspresso - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Compose support

+

Jetpack Compose support consists of two parts: Kakao Compose library and Kaspresso Interceptors mechanism.

+

Kakao Compose library

+

All detailed information is available in the README of the library.

+

Jetpack Compose support is provided by a separate module to not force developers to up their minSDK version to 21.

+

So, first of all, add a dependency to build.gradle: +

dependencies {
+    androidTestImplementation "com.kaspersky.android-components:kaspresso-compose-support:<latest_version>"
+}
+

+

In a nutshell, let's see at how Kakao Compose DSL looks like: +

// Screen class
+class ComposeMainScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
+    ComposeScreen<ComposeMainScreen>(
+        semanticsProvider = semanticsProvider,
+        // Screen in Kakao Compose can be a Node too due to 'viewBuilderAction' param.
+        // 'viewBuilderAction' param is nullable.
+        viewBuilderAction = { hasTestTag("ComposeMainScreen") }
+) {
+
+    // You can set clear parent-child relationship due to 'child' extension
+    // Here, 'simpleFlakyButton' is a child of 'ComposeMainScreen' (that is Node too)
+    val simpleFlakyButton: KNode = child {
+        hasTestTag("main_screen_simple_flaky_button")
+    }
+}
+
+// This annotation is here to make the test is appropriate for JVM environment (with Robolectric)
+@RunWith(AndroidJUnit4::class)
+// Test class declaration
+class ComposeSimpleFlakyTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.withComposeSupport()
+) {
+
+    // Special rule for Compose tests
+    @get:Rule
+    val composeTestRule = createAndroidComposeRule<JetpackComposeActivity>()
+
+    // Test DSL. It's so similar to Kakao or Kautomator DSL
+    @Test
+    fun test() = run {
+        step("Open Flaky screen") {
+            onComposeScreen<ComposeMainScreen>(composeTestRule) {
+                simpleFlakyButton {
+                    assertIsDisplayed()
+                    performClick()
+                }
+            }
+        }
+
+        step("Click on the First button") {
+            onComposeScreen<ComposeSimpleFlakyScreen>(composeTestRule) {
+                firstButton {
+                    assertIsDisplayed()
+                    performClick()
+                }
+            }
+        }
+
+        // ...
+    }
+}
+
+Again, all related to DSL information is available in the docs.

+

Kaspresso Interceptors mechanism

+

Interceptors are one of the main advantages and powers of Kaspresso library.
+How interceptors work is described +at the article (look the chapter "Flaky tests and logging").

+

The same principles are using in Kaspresso for Jetpack Compose. +Let's enumerate default interceptors that work under the hood by default when you write tests with Kaspresso.

+

Behavior interceptors

+
    +
  1. FailureLoggingSemanticsBehaviorInterceptor
    + Build the clear and undestandable exception in case of the test failure.
  2. +
  3. FlakySafeSemanticsBehaviorInterceptor
    + Tries to repeat the failed action or assertion during defined timeout. All params for this interceptor are at FlakySafetyParams.
  4. +
  5. SystemDialogSafetySemanticsBehaviorInterceptor
    + Eliminates various system dialogs that prevent correct execution of a test.
  6. +
  7. AutoScrollSemanticsBehaviorInterceptor
    + Performs autoscrolling to an element if the element is not visible on the screen.
  8. +
  9. ElementLoaderSemanticsBehaviorInterceptor
    + Requests the related SemanticNodeInteraction using saved Matcher when the element is not found.
  10. +
+

Watcher interceptors

+

LoggingSemanticsWatcherInterceptor. The Interceptor produces human-readable logs. The example: +

I/KASPRESSO: TEST STEP: "1. Open Flaky screen" in ComposeSimpleFlakyTest SUCCEED. It took 0 minutes, 0 seconds and 212 millis. 
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: TEST STEP: "2. Click on the First button" in ComposeSimpleFlakyTest
+I/KASPRESSO: Operation: Check=IS_DISPLAYED(description={null}).
+    ComposeInteraction: matcher: (hasParentThat(TestTag = 'simple_flaky_screen_container')) && (TestTag = 'simple_flaky_screen_simple_first_button'); position: 0; useUnmergedTree: false.
+I/KASPRESSO: Reloading of the element is started
+I/KASPRESSO: Reloading of the element is finished
+I/KASPRESSO: Repeat action again with the reloaded element
+I/KASPRESSO: Operation: Check=IS_DISPLAYED(description={null}).
+    ComposeInteraction: matcher: (hasParentThat(TestTag = 'simple_flaky_screen_container')) && (TestTag = 'simple_flaky_screen_simple_first_button'); position: 0; useUnmergedTree: false.
+I/KASPRESSO: SemanticsNodeInteraction autoscroll successfully performed.
+I/KASPRESSO: Operation: Check=IS_DISPLAYED(description={null}).
+    ComposeInteraction: matcher: (hasParentThat(TestTag = 'simple_flaky_screen_container')) && (TestTag = 'simple_flaky_screen_simple_first_button'); position: 0; useUnmergedTree: false.
+I/KASPRESSO: Operation: Perform=PERFORM_CLICK(description={null}).
+    ComposeInteraction: matcher: (hasParentThat(TestTag = 'simple_flaky_screen_container')) && (TestTag = 'simple_flaky_screen_simple_first_button'); position: 0; useUnmergedTree: false.
+I/KASPRESSO: TEST STEP: "2. Click on the First button" in ComposeSimpleFlakyTest SUCCEED. It took 0 minutes, 0 seconds and 123 millis. 
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: TEST STEP: "3. Click on the Second button" in ComposeSimpleFlakyTest
+

+

Caveats

+

Remember, that Jetpack Compose and all relative tools are developing. +It means Jetpack Compose is not learned very well and some things can be unexpected after "Old fashioned View World" experience. +Let me show the interesting case.

+

For example, this code +

composeSimpleFlakyScreen(composeTestRule) {
+    firstButton {
+        performClick()
+    }
+}
+
+can be the source of flakiness behavior if firstButton is located in non visible for a user area +(you just need to scroll to see the element).

+

But, this code will always work stably: +

composeSimpleFlakyScreen(composeTestRule) {
+    firstButton {
+        assertIsDisplayed()
+        performClick()
+    }
+}
+

+

The explanation is in the nature of SemanticsNode Tree and Jetpack Compose. firstButton is a Node and presented in the Tree. +It means that performClick() may work and nothing bad doesn't happen. But, firstButton is not visible physically and a real click doesn't occur. +Such behavior causes the crash of a test a little bit later.
+But, assertIsDisplayed() check doesn't pass on the first try (we don't see the element on the screen) and +launches work of all Interceptors including Autoscroll interceptor which scrolls the Screen to the desired element.

+

Please, share your experience to help other developers.

+

What else

+

Configuration

+

Jetpack Compose support is fully configurable. Have a look at various options to configure: +

// We edit only semanticsBehaviorInterceptors
+// Now, semanticsBehaviorInterceptors contains only FailureLoggingSemanticsBehaviorInterceptor
+class ComposeCustomizeTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.withComposeSupport { composeBuilder ->
+        composeBuilder.semanticsBehaviorInterceptors = composeBuilder.semanticsBehaviorInterceptors.filter {
+            it is FailureLoggingSemanticsBehaviorInterceptor
+        }.toMutableList()
+    }
+)
+
+// We edit flakySafetyParams and semanticsBehaviorInterceptors
+// Also, we change semanticsBehaviorInterceptors where we exclude SystemDialogSafetySemanticsBehaviorInterceptor
+class ComposeCustomizeTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.withComposeSupport(
+        // It's very important to change flakySafetyParams in customize section
+        // Otherwise, all interceptors will use a default version of flakySafetyParams
+        customize = {
+            flakySafetyParams = FlakySafetyParams.custom(timeoutMs = 5000, intervalMs = 1000)
+        },
+        lateComposeCustomize = { composeBuilder ->
+            composeBuilder.semanticsBehaviorInterceptors = composeBuilder.semanticsBehaviorInterceptors.filter {
+                it !is SystemDialogSafetySemanticsBehaviorInterceptor
+            }.toMutableList()
+        }
+    ).apply {
+        // Remember, It's better to customize ComposeSupport only after Kaspresso customizing
+        // Because ComposeSupport interceptors can be dependent on some Kaspresso entities
+        // For example, changing flakySafetyParams in this section will not affect ComposeSupport interceptors
+    }
+)
+
+// There is another way to do exactly the same
+class ComposeCustomizeTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple {
+        flakySafetyParams = FlakySafetyParams.custom(timeoutMs = 5000, intervalMs = 1000)
+    }.apply {
+        addComposeSupport { composeBuilder ->
+            composeBuilder.semanticsBehaviorInterceptors = composeBuilder.semanticsBehaviorInterceptors.filter {
+                it !is SystemDialogSafetySemanticsBehaviorInterceptor
+            }.toMutableList()
+        }
+    }
+)
+

+

Robolectric support

+

You can run your Compose tests on the JVM environment with Robolectric.
+Run ComposeSimpleFlakyTest (from "kaspresso-sample" module) on the JVM right now: +

./gradlew :samples:kaspresso-compose-support-sample:testDebugUnitTest --info --tests "com.kaspersky.kaspresso.composesupport.sample.test.ComposeSimpleFlakyTest"  
+
+All information about Robolectric support is available here.

+

Compose is compatible with all sweet Kaspresso extensions

+

Sweet Kaspresso extensions means using of the such constructions as:

+
    +
  1. flakySafely
  2. +
  3. continuously
  4. +
+

The support of some constructions is in progress: issue-317.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Kaspresso_Allure/index.html b/Wiki/Kaspresso_Allure/index.html new file mode 100644 index 000000000..cd352ceeb --- /dev/null +++ b/Wiki/Kaspresso_Allure/index.html @@ -0,0 +1,1570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Kaspresso with Allure - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Kaspresso-allure support

+

What's new

+

In the 1.3.0 Kaspresso release the allure-framework support was added. Now it is very easy to generate pretty test reports using both Kaspresso and Allure frameworks.

+

In this release, the file-managing classes family that is responsible for providing files for screenshots and logs has been refactored for better usage and extensibility. This change has affected the old classes that are deprecated now (see package com.kaspersky.kaspresso.files). Usage example: CustomizedSimpleTest.

+

Also, the following interceptors were added:

+
    +
  1. VideoRecordingInterceptor. Tests video recording interceptor (please note that it was fully tested on emulators with android api 29 and older).
  2. +
  3. DumpViewsInterceptor. Interceptor that dumps XML-representation of view hierarchy in case of a test failure.
  4. +
+

In the package com.kaspersky.components.alluresupport.interceptors, there are special Kaspresso interceptors helping to link and process files for Allure-report.

+

How to use

+

First of all, add the following Gradle dependency and Allure runner to your project's gradle file to include allure-support Kaspresso module: +

android {
+    defaultConfig {
+        //...    
+        testInstrumentationRunner "com.kaspersky.kaspresso.runner.KaspressoRunner"
+    }
+    //...
+}
+
+dependencies {
+    //...
+    androidTestImplementation "com.kaspersky.android-components:kaspresso-allure-support:<latest_version>"
+}
+
+Next, use special withForcedAllureSupport function in your TestCase constructor or in your TestCaseRule to turn on all available Allure-supporting interceptors: +
class AllureSupportTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.withForcedAllureSupport()
+) {
+
+}
+
+If you want to specify the parameters or add more interceptors you can use addAllureSupport function: +
class AllureSupportCustomizeTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple(
+        customize = {
+            videoParams = VideoParams(bitRate = 10_000_000)
+            screenshotParams = ScreenshotParams(quality = 1)
+        }
+    ).addAllureSupport().apply {
+        testRunWatcherInterceptors.apply {
+            add(object : TestRunWatcherInterceptor {
+                override fun onTestFinished(testInfo: TestInfo, success: Boolean) {
+                    viewHierarchyDumper.dumpAndApply("ViewHierarchy") { attachViewHierarchyToAllureReport() }
+                }
+            })
+        }
+    }
+) {
+...
+}
+
+If you don't need all of these interceptors providing by withForcedAllureSupport and addAllureSupport functions then you may add only interceptors that you prefer. But please note that AllureMapperStepInterceptor.kt is mandatory for Allure support work. For example, if you don't need videos and view hierarchies after test failures then you can do something like: +
class AllureSupportCustomizeTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple().apply {
+        stepWatcherInterceptors.addAll(
+            listOf(
+                ScreenshotStepInterceptor(screenshots),
+                AllureMapperStepInterceptor()
+            )
+        )
+        testRunWatcherInterceptors.addAll(
+            listOf(
+                DumpLogcatTestInterceptor(logcatDumper),
+                ScreenshotTestInterceptor(screenshots),
+            )
+        )
+    }
+) {
+...
+}
+
+kaspresso-allure-support-sample is available to watch, to launch and to experiment with all of this staff.

+

Watch result

+

So you added the list of needed Allure-supporting interceptors to your Kaspresso configuration and launched the test. After the test finishes there will be sdcard/allure-results dir created on the device with all the files processed to be included to Allure-report.

+

This dir should be moved from the device to the host machine which will do generate the report.

+

For example, you can use adb pull command on your host for this. Let say you want to locate the data for the report at /Users/username/Desktop/allure-results, so you call: +

adb pull /sdcard/allure-results /Users/username/Desktop
+
+If there are few devices connected to yout host you should specify the needed device id. To watch the list of connected devices you can call: +
adb devices
+
+The output will be something like: +
List of devices attached
+CLCDU18508004769    device
+emulator-5554   device
+
+Select the needed device and call: +
adb -s emulator-5554 pull /sdcard/allure-results /Users/username/Desktop
+
+And that's it, the allure-results dir with all the test resources is now at /Users/username/Desktop.

+

Now, we want to generate and watch the report. The Allure server must be installed on our machine for this. To find out how to do it with all the details please follow the Allure docs.

+

For example to install Allure server on MacOS we can use the following command: +

brew install allure
+
+Now we are ready to generate and watch the report, just call: +
allure serve /Users/username/Desktop/allure-results
+
+Next, the Allure server generates the html-page representing the report and puts it to temp dir in your system. You will see the report opening in the new tab in your browser (the tab is opening automatically).

+

If you want to save the generated html-report to a specific dir for future use you can just call: +

allure generate -o ~/kaspresso-allure-report /Users/username/Desktop/allure-results
+
+And to watch it then in your browser you just call: +
allure open ~/kaspresso-allure-report
+
+After all of this actions you see something like: +

+

Details for succeeded test: +

+

Details for failed test: +

+

Details that you need to know

+

By default, Kaspresso-Allure introduces additional timeouts to assure the correctness of a Video recording as much as possible. To summarize, these timeouts increase a test execution time by 5 seconds. +You are free to change these values by customizing videoParams in Kaspresso.Builder. See the example above.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Kaspresso_Robolectric/index.html b/Wiki/Kaspresso_Robolectric/index.html new file mode 100644 index 000000000..d01795639 --- /dev/null +++ b/Wiki/Kaspresso_Robolectric/index.html @@ -0,0 +1,1547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Kaspresso with Robolectric - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Kaspresso tests running on the JVM with Robolectric

+

Main purpose

+

Since Robolectric 4.0, we can also run Espresso-like tests also on the JVM with Robolectric. +That is part of the Project nitrogen from Google (which became Unified Test Platform), where they want to allow developers to write UI test once, and run them everywhere.

+

However, before Kaspresso 1.3.0, if you tried to run Kaspresso-like test extending TestCase on the JVM with Robolectric, you got the following error: +

java.lang.NullPointerException
+    at androidx.test.uiautomator.QueryController.<init>(QueryController.java:95)
+    at androidx.test.uiautomator.UiDevice.<init>(UiDevice.java:109)
+    at androidx.test.uiautomator.UiDevice.getInstance(UiDevice.java:261)
+    at com.kaspersky.kaspresso.kaspresso.Kaspresso$Builder.<init>(Kaspresso.kt:297)
+    at com.kaspersky.kaspresso.kaspresso.Kaspresso$Builder$Companion.simple(Kaspresso.kt:215)
+    ...
+
+That is because Robolectric is just compatible with Espresso and not with UI Automator.

+

Now, all Kaspresso tests are allowed to be executed correctly on the JVM with Robolectric with the following restrictions:

+
    +
  1. Easy configuration of your project according to Robolectric guideline.
  2. +
  3. Not possible to use adb-server because there is no a term like "Desktop" on the JVM environment. Tests that use adb-server will crash on the JVM with Robolectric with very explaining error message.
  4. +
  5. Not possible to work with UiDevice and UiAutomation classes. That's why a lot of (not all!) implementations in Device will crash on the JVM with Robolectric with NotSupportedInstrumentalTestException.
  6. +
  7. Non working Kautomator. Mentioned problem with UiDevice and UiAutomation classes affect the entire Kautomator. So, tests using Kautomator will crash on the JVM with Robolectric with KautomatorInUnitTestException.
  8. +
  9. Interceptors that use UiDevice, UiAutomation or adb-server are turning off on the JVM with Robolectric automatically.
  10. +
  11. DocLocScreenshotTestCase will crash on the JVM with Robolectric with DocLocInUnitTestException.
  12. +
+

Usage

+

To create a test that can run on a device/emulator and on the JVM, we recommend to create a sharedTest folder, and configure sourceSets in gradle.

+
sourceSets {
+   ...
+   //configure shared test folder
+   val sharedTestFolder = "src/sharedTest/kotlin"
+   val androidTest by getting {
+       java.srcDirs("src/androidTest/java", sharedTestFolder)
+   }
+   val test by getting {
+       java.srcDirs("src/test/java", sharedTestFolder)
+   }
+}
+
+

It is also important that such tests use @RunWith(AndroidJUnit4::class), since it is required by Robolectric.

+

In order to run your shared tests as Unit Tests on the JVM, you need to run a command looking like this: +

./gradlew :MODULE:testVARIANTUnitTest --info --tests "PACKAGE.CLASS"
+

+

For example, to run the sample RobolectricTest on the JVM you need to run: +

./gradlew :samples:kaspresso-sample:testDebugUnitTest --info --tests "com.kaspersky.kaspressample.sharedtest.SharedSimpleFlakyTest"
+

+

To run them on a device/emulator, the command to run would look like this: +

./gradlew :MODULE:connectedVARIANTAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=PACKAGE.CLASS
+

+

For instance, to run the sample SharedTest on a device/emulator, you need to run: +

./gradlew :samples:kaspresso-sample:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.kaspersky.kaspressample.sharedtest.SharedSimpleFlakyTest
+

+

Accommodation of tests to work on the JVM (with Robolectric) environment

+

We've prepared a bunch of tools and advices to accommodate your tests for the JVM (with Robolectric) environment.

+

Let's consider the most popular problem when a test uses a class containing calls to UiDevice/UiAutomation/AdbServer or other not working in JVM environment things.

+

For example, your test looks like below: +

@RunWith(AndroidJUnit4::class)
+class FailingSharedTest : TestCase() {
+
+    @get:Rule
+    val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        Manifest.permission.WRITE_EXTERNAL_STORAGE,
+        Manifest.permission.READ_EXTERNAL_STORAGE
+    )
+
+    @get:Rule
+    val activityTestRule = ActivityTestRule(DeviceSampleActivity::class.java, false, true)
+
+    @Test
+    fun exploitSampleTest() =
+        run {
+            step("Press Home button") {
+                device.exploit.pressHome()
+            }
+            //...
+        }
+}
+

+

device.exploit.pressHome() calls UiDevice under the hood and it leads to a crash the JVM environment.

+

There is following possible solution: +

// change an implementation of Exploit class
+@RunWith(AndroidJUnit4::class)
+class FailingSharedTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple {
+        exploit = 
+            if (isAndroidRuntime) ExploitImpl() // old implementation
+            else ExploitUnit() // new implementation without UiDevice
+    }
+) { ... }
+
+// isAndroidRuntime property is available in Kaspresso.Builder.
+

+

Also, if your custom Interceptor uses UiDevice/UiAutomation/AdbServer then you can turn off this Interceptor for JVM. The example: +

class KaspressoConfiguringTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple {
+        viewBehaviorInterceptors = if (isAndroidRuntime) mutableListOf(
+           YourCustomInterceptor(),
+           FlakySafeViewBehaviorInterceptor(flakySafetyParams, libLogger)
+       ) else mutableListOf(
+           FlakySafeViewBehaviorInterceptor(flakySafetyParams, libLogger)
+       )
+    }
+) { ... }
+

+

Of course, there is a very obvious last option. Just don't include the test in a set of Unit tests.

+

Further remarks

+

As of Robolectric 4.8.1, there are some limitations to sharedTest: those tests run flawless on an emulator/device, but fail on the JVM

+
    +
  1. Robolectric-Espresso supports Idling resources, but doesn't support posting delayed messages to the Looper
  2. +
  3. Robolectric-Espresso will not support tests that start new activities (i.e. activity jumping)
  4. +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Kaspresso_configuration/index.html b/Wiki/Kaspresso_configuration/index.html new file mode 100644 index 000000000..21b899955 --- /dev/null +++ b/Wiki/Kaspresso_configuration/index.html @@ -0,0 +1,2021 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Kaspresso configuration - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Kaspresso configurator

+

Kaspresso class - is a single point to set Kaspresso parameters.
+A developer can customize Kaspresso by setting Kaspresso.Builder at constructors of TestCase, BaseTestCase, TestCaseRule, BaseTestCaseRule.
+The example: +

class SomeTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple {
+        beforeEachTest {
+            testLogger.i("The beginning")
+        }
+        afterEachTest {
+            testLogger.i("The end")
+        }
+    }
+) {
+    // your test
+}
+

+

Structure

+

Kaspresso configuration contains:

+

Kakao clicks

+

Kaspresso provides the possibility to override Espresso custom clicks. +Kakao library provides a set of prepared custom clicks which improves the stability of the tests especially on the devices under high load.

+

All details about the problem and solutions are described in Kakao documentation.

+

The example of how to apply the custom clicks in your test is presented in CustomClickTest. +

class ClickTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple(
+        customize = {
+            clickParams = ClickParams.kakaoVisual()
+        }
+    )
+) {
+    // your test
+}
+

+

Kaspresso provides the next prepared options to customise clicks: +1. ClickParams.kakaoVisual()' - Kakao clicks with visualisation. +2.ClickParams.kakao()' - Kakao clicks. +3. `ClickParams.default()' - Espresso clicks. Using by default.

+

Loggers

+

Kaspresso provides two loggers: libLogger and testLogger. +libLogger - inner Kaspresso logger
+testLogger - logger that is available for developers in tests.
+The last one is accessible by testLogger property in test sections (before, after, init, transform, run) in the test DSL (by TestContext class).
+Also, it is available while setting Kaspresso.Builder if you want to add it to your custom interceptors, for example.

+

Kaspresso interceptors based on Kakao/Kautomator Interceptors.

+

These interceptors were introduced to simplify and uniform using of Kakao interceptors and Kautomator interceptors.

+

Important moment about a mixing of Kaspresso interceptors and Kakao/Kautomator interceptors.
+Kaspresso interceptors will not work if you set your custom Kakao interceptors by calling of Kakao.intercept method in the test or set your custom Kautomator interceptors by calling of Kautomator.intercept in the test.
+If you set your custom Kakao interceptors for concrete Screen or KView and set argument isOverride in true then Kaspresso interceptors will not work for concrete Screen or KView fully. The same statement is right for Kautomator where a developer interacts with UiScreen and UiBaseView.

+

Kaspresso interceptors can be divided into two types:

+
    +
  1. Behavior Interceptors - are intercepting calls to ViewInteraction, DataInteraction, WebInteraction, UiObjectInteraction, UiDeviceInteraction and do some stuff.
    + Attention, we are going to consider some important notes about Behavior Interceptors at the end of this document.
  2. +
  3. Watcher Interceptors - are intercepting calls to ViewAction, ViewAssertion, Atom, WebAssertion, UiObjectAssertion, UiObjectAction, UiDeviceAssertion, UiDeviceAction and do some stuff.
  4. +
+

Let's expand mentioned Kaspresso interceptors types:

+
    +
  1. Behavior Interceptors
      +
    1. viewBehaviorInterceptors - intercept calls to ViewInteraction#perform and ViewInteraction#check
    2. +
    3. dataBehaviorInterceptors - intercept calls to DataInteraction#check
    4. +
    5. webBehaviorInterceptors - intercept calls to Web.WebInteraction<R>#perform and Web.WebInteraction<R>#check
    6. +
    7. objectBehaviorInterceptors - intercept calls to UiObjectInteraction#perform and UiObjectInteraction#check
    8. +
    9. deviceBehaviorInterceptors - intercept calls to UiDeviceInteraction#perform and UiDeviceInteraction#check
    10. +
    +
  2. +
  3. Watcher Interceptors
      +
    1. viewActionWatcherInterceptors - do some stuff before android.support.test.espresso.ViewAction.perform is actually called
    2. +
    3. viewAssertionWatcherInterceptors - do some stuff before android.support.test.espresso.ViewAssertion.check is actually called
    4. +
    5. atomWatcherInterceptors - do some stuff before android.support.test.espresso.web.model.Atom.transform is actually called
    6. +
    7. webAssertionWatcherInterceptors - do some stuff before android.support.test.espresso.web.assertion.WebAssertion.checkResult is actually called
    8. +
    9. objectWatcherInterceptors - do some stuff before UiObjectInteraction.perform or UiObjectInteraction.check is actually called
    10. +
    11. deviceWatcherInterceptors - do some stuff before UiDeviceInteraction.perform or UiDeviceInteraction.check is actually called
    12. +
    +
  4. +
+

Please, remember! Behavior and watcher interceptors work under the hood in every action and assertion of every View of Kakao and Kautomator by default in Kaspresso.

+

Special Kaspresso interceptors

+

These interceptors are not based on some lib. Short description:

+
    +
  1. stepWatcherInterceptors - an interceptor of Step lifecycle actions
  2. +
  3. testRunWatcherInterceptors - an interceptor of entire Test lifecycle actions
  4. +
+

As you noticed these interceptors are a part of Watcher Interceptors, also.

+

BuildStepReportWatcherInterceptor

+

This watcher interceptor by default is included into Kaspresso configurator to collect your tests steps information for further processing in tests orchestrator.
+By default this interceptor is based on AllureReportWriter (if you don't know what Allure is you should really check on it).
+This report writer works with each TestInfo after test finishing, converts its steps information into Allure's steps info JSON, and then prints JSON into LogCat in the following format:

+
I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: TEST PASSED
+I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: #AllureStepsInfoJson#: [{"attachments":[],"name":"My step 1","parameters":[],"stage":"finished","start":1568790287246,"status":"passed", "steps":[],"stop":1568790288184}]
+
+

This logs should be processed by your test orchestrator (e.g. Marathon). +If you use Marathon you should know that the it requires +some additional modifications to support processing this logs and doesn't work as expected at the current moment. But we are working hard on it.

+

Default actions in before/after sections

+

Sometimes, a developer wishes to put some actions repeating in all tests before/after into a single place to simplify the maintenance of tests.
+You can make a remark that there are @beforeTest/@afterTest annotations to resolve mentioned tasks. But the developer doesn't have an access to BaseTestContext in those methods. +That's why we have introduced special default actions that you can set in constructor by Kaspresso.Builder.
+The example how to implement default actions in Kaspresso.Builder is:
+

open class YourTestCase : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple {
+        beforeEachTest {
+            testLogger.i("beforeTestFirstAction")
+        }
+        afterEachTest {
+            testLogger.i("afterTestFirstAction")
+        }
+    }
+)
+
+The full signature of beforeEachTest is: +
beforeEachTest(override = true, action = {
+    testLogger.i("beforeTestFirstAction")
+})
+
+afterEachTest is similar to beforeEachTest.
+If you set override in false then the final beforeAction will be beforeAction of the parent TestCase plus current action. Otherwise, final beforeAction will be only current action. +How it's work and how to override (or just extend) default action, please, +observe the example.

+

Device

+

Device instance. Detailed info is at Device wiki.

+

AdbServer

+

AdbServer instance. Detailed info is at AdbServer wiki.

+

Kaspresso configuring and Kaspresso interceptors example

+

The example of how to configure Kaspresso and how to use Kaspresso interceptors is in here.

+

Default Kaspresso settings

+

BaseTestCase, TestCase, BaseTestCaseRule, TestCaseRule are using default customized Kaspresso (Kaspresso.Builder.simple builder).
+Most valuable features of default customized Kaspresso are below.

+

Logging

+

Just start SimpleTest. Next, you will see those logs: +

I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: BEFORE TEST SECTION
+I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: TEST SECTION
+I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: TEST STEP: "1. Open Simple Screen" in SimpleTest
+I/KASPRESSO_SPECIAL: I am kLogger
+I/ViewInteraction: Checking 'com.kaspersky.kaspresso.proxy.ViewAssertionProxy@95afab5' assertion on view (with id: com.kaspersky.kaspressample:id/activity_main_button_next)
+I/KASPRESSO: Check view has effective visibility=VISIBLE on AppCompatButton(id=activity_main_button_next;text=Next;)
+I/KASPRESSO: single click on AppCompatButton(id=activity_main_button_next;text=Next;)
+I/KASPRESSO: TEST STEP: "1. Open Simple Screen" in SimpleTest SUCCEED. It took 0 minutes, 0 seconds and 618 millis. 
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: TEST STEP: "2. Click button_1 and check button_2" in SimpleTest
+I/KASPRESSO: single click on AppCompatButton(id=button_1;text=Button 1;)
+I/ViewInteraction: Checking 'com.kaspersky.kaspresso.proxy.ViewAssertionProxy@9f38781' assertion on view (with id: com.kaspersky.kaspressample:id/button_2)
+I/KASPRESSO: Check view has effective visibility=VISIBLE on AppCompatButton(id=button_2;text=Button 2;)
+I/KASPRESSO: TEST STEP: "2. Click button_1 and check button_2" in SimpleTest SUCCEED. It took 0 minutes, 0 seconds and 301 millis. 
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: TEST STEP: "3. Click button_2 and check edit" in SimpleTest
+I/KASPRESSO: single click on AppCompatButton(id=button_2;text=Button 2;)
+I/ViewInteraction: Checking 'com.kaspersky.kaspresso.proxy.ViewAssertionProxy@ad01abd' assertion on view (with id: com.kaspersky.kaspressample:id/edit)
+I/KASPRESSO: Check view has effective visibility=VISIBLE on AppCompatEditText(id=edit;text=Some text;)
+E/KASPRESSO: Failed to interact with view matching: (with id: com.kaspersky.kaspressample:id/edit) because of AssertionFailedError
+I/ViewInteraction: Checking 'com.kaspersky.kaspresso.proxy.ViewAssertionProxy@d0f1c0a' assertion on view (with id: com.kaspersky.kaspressample:id/edit)
+I/KASPRESSO: Check view has effective visibility=VISIBLE on AppCompatEditText(id=edit;text=Some text;)
+I/ViewInteraction: Checking 'com.kaspersky.kaspresso.proxy.ViewAssertionProxy@3b62c7b' assertion on view (with id: com.kaspersky.kaspressample:id/edit)
+I/KASPRESSO: Check with string from resource id: <2131558461> on AppCompatEditText(id=edit;text=Some text;)
+I/KASPRESSO: TEST STEP: "3. Click button_2 and check edit" in SimpleTest SUCCEED. It took 0 minutes, 2 seconds and 138 millis. 
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: TEST STEP: "4. Check all possibilities of edit" in SimpleTest
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: TEST STEP: "4.1. Change the text in edit and check it" in SimpleTest
+I/KASPRESSO: replace text on AppCompatEditText(id=edit;text=Some text;)
+I/KASPRESSO: type text(111) on AppCompatEditText(id=edit;)
+I/ViewInteraction: Checking 'com.kaspersky.kaspresso.proxy.ViewAssertionProxy@dbd9c8' assertion on view (with id: com.kaspersky.kaspressample:id/edit)
+I/KASPRESSO: Check with text: is "111" on AppCompatEditText(id=edit;text=111;)
+I/KASPRESSO: TEST STEP: "4.1. Change the text in edit and check it" in SimpleTest SUCCEED. It took 0 minutes, 0 seconds and 621 millis. 
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: TEST STEP: "4.2. Change the text in edit and check it. Second check" in SimpleTest
+I/KASPRESSO: replace text on AppCompatEditText(id=edit;text=111;)
+I/KASPRESSO: type text(222) on AppCompatEditText(id=edit;)
+I/ViewInteraction: Checking 'com.kaspersky.kaspresso.proxy.ViewAssertionProxy@b8ca74' assertion on view (with id: com.kaspersky.kaspressample:id/edit)
+I/KASPRESSO: Check with text: is "222" on AppCompatEditText(id=edit;text=222;)
+I/KASPRESSO: TEST STEP: "4.2. Change the text in edit and check it. Second check" in SimpleTest SUCCEED. It took 0 minutes, 0 seconds and 403 millis. 
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: TEST STEP: "4. Check all possibilities of edit" in SimpleTest SUCCEED. It took 0 minutes, 1 seconds and 488 millis. 
+I/KASPRESSO: ___________________________________________________________________________
+I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: AFTER TEST SECTION
+I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: TEST PASSED
+I/KASPRESSO: ---------------------------------------------------------------------------
+I/KASPRESSO: #AllureStepsInfoJson#: [{"attachments":[],"name":"My step 1","parameters":[],"stage":"finished","start":1568790287246,"status":"passed", "steps":[],"stop":1568790288184}]
+I/KASPRESSO: ---------------------------------------------------------------------------
+
+Pretty good.

+

Defense from flaky tests

+

If a failure occurs then Kaspresso tries to fix it using a big set of diverse ways.
+This defense works for every action and assertion of each View of Kakao and Kautomator! You just need to extend your test class from TestCase (BaseTestCase) or to set TestCaseRule(BaseTestCaseRule) in your test.
+More detailed info about some ways of defense is below

+

Interceptors

+

Interceptors turned by default:

+
    +
  1. Watcher interceptors
  2. +
  3. Behavior interceptors
  4. +
  5. Kaspresso interceptors
  6. +
  7. BuildStepReportWatcherInterceptor
  8. +
+

So, all features described above are available thanks to these interceptors.

+

Some words about Behavior Interceptors

+

Any lib for ui-tests is flaky. It's a hard truth of life. Any action/assert in your test may fail for some undefined reason.

+

What general kinds of flaky errors exist:

+
    +
  1. Common flaky errors that happened because Espresso/UI Automator was in a bad mood =)
    + That's why Kaspresso wraps all actions/assertions of Kakao/Kautomator and handles set of potential flaky exceptions. + If an exception happened then Kaspresso attempts to repeat failed actions/assert for 10 seconds. Such handling rescues developers of any flaky action/assert.
    + The details are available at flakysafety and examples are here.
  2. +
  3. The reason of a failure is non visibility of a View. In most cases you just need to scroll a parent layout to make the View visible. So, Kaspresso tries to perform it in auto mode.
    + The details are available at autoscroll.
  4. +
  5. Also, Kaspresso attempts to remove all system dialogs if it prevents the test execution.
    + The details are available at systemsafety.
  6. +
+

These handlings are possible thanks to BehaviorInterceptors. Also, you can set your custom processing by Kaspresso.Builder. But remember, the order of BehaviorInterceptors is significant: the first item will be at the lowest level of intercepting chain, and the last item will be at the highest level.

+

Let's consider the work principle of BehaviorInterceptors over Kakao interceptors. The first item actually wraps the androidx.test.espresso.ViewInteraction.perform call, the second item wraps the first item, and so on.
+Have a glance at the order of BehaviorInterceptors enabled by default in Kaspresso over Kakao. It's:

+
    +
  1. AutoScrollViewBehaviorInterceptor
  2. +
  3. SystemDialogSafetyViewBehaviorInterceptor
  4. +
  5. FlakySafeViewBehaviorInterceptor
  6. +
+

Under the hood, all Kakao actions and assertions first of all call FlakySafeViewBehaviorInterceptor that calls SystemDialogSafetyViewBehaviorInterceptor and that calls AutoScrollViewBehaviorInterceptor.
+If a result of AutoScrollViewBehaviorInterceptor handling is an error then SystemDialogSafetyViewBehaviorInterceptor attempts to handle received error. If a result of SystemDialogSafetyViewBehaviorInterceptor handling is an error too then FlakySafeViewBehaviorInterceptor attempts to handle received the error.
+To simplify the discussed topic we have drawn a picture:

+

+

Main section enrichers

+

Developer also can extends parametrized tests functionality by providing MainSectionEnricher in BaseTestCase or BaseTestCaseRule. +The main idea of enrichers - allow adding additional test case's steps before and after the main section's run block.

+

All you need to do is:

+
    +
  1. Define your enricher implementation for MainSectionEnricher interface;
  2. +
+
class LoggingMainSectionEnricher : MainSectionEnricher<TestCaseData> {
+    ...
+
+}
+
+

Here, TestCaseData is the same data type as in your BaseTestCase implementation.

+
    +
  1. Override beforeMainSectionRun or/and afterMainSectionRun methods to add your before/after actions;
  2. +
+
class LoggingMainSectionEnricher : MainSectionEnricher<TestCaseData> {
+
+    override fun TestContext<TestCaseData>.beforeMainSectionRun(testInfo: TestInfo) {
+        testLogger.d("Before main section run... | ${testInfo.testName}")
+        step("Check users count...") {
+            testLogger.d("Check users count: ${data.users.size}")
+        }
+    }
+
+    override fun TestContext<TestCaseData>.afterMainSectionRun(testInfo: TestInfo) {
+        testLogger.d("After main section run... | ${testInfo.testName}")
+        step("Check posts count...") {
+            testLogger.d("Check posts count: ${data.posts.size}")
+        }
+    }
+
+}
+
+

In beforeMainSectionRun and afterMainSectionRun methods you have full access to TestContext<TestCaseData properties and methods, +so you can use logger, add test case's steps and so on. Also, this methods received TestInfo parameter.

+
    +
  1. Add your enrichers into your BaseTestCase implementation.
  2. +
+
class EnricherBaseTestCase : BaseTestCase<TestCaseDsl, TestCaseData>(
+    kaspresso = Kaspresso.Builder.default(),
+    dataProducer = { action -> TestCaseDataCreator.initData(action) },
+    mainSectionEnrichers = listOf(
+        LoggingMainSectionEnricher(),
+        AnalyticsMainSectionEnricher()
+    )
+)
+
+

After this manipulations your described actions will be executed before or after main section's run block.

+

Pulling the artifacts from the device to the host

+

Depending on your test configuration, useful artifacts may remain on the device after test finish: screenshots, reports, videos, etc. +In order to pull them off the device you could program a script, which would be executed after the completion of the test run on CI. With Kaspresso, +you can simplify this process. To do this, you need to configure the artifactsPullParams variable in the Kaspresso Builder. Example:

+
class SomeTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple {
+        artifactsPullParams = ArtifactsPullParams(enabled = true, destinationPath = "artifacts/", artifactsRegex = Regex("(screenshots)|(videos)"))
+    }
+) {
+    ...
+}
+
+

To make this work, you need to start the ADB server before running the test. After the test is completed, the artifacts will be located by the path specified in the destinationPath +argument relative to the working directory from which the ADB server was launched.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Kautomator-wrapper_over_UI_Automator/index.html b/Wiki/Kautomator-wrapper_over_UI_Automator/index.html new file mode 100644 index 000000000..be1746b05 --- /dev/null +++ b/Wiki/Kautomator-wrapper_over_UI_Automator/index.html @@ -0,0 +1,1908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Kautomator. Wrapper over UI Automator - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Kautomator: wrapper over UI Automator

+

Kautomator - Nice and simple DSL for UI Automator in Kotlin that allows to accelerate UI Automator to amazing.
+Inspired by Kakao and russian talk about UI Automator (thanks to Svetlana Smelchakova).

+

Introduction

+

Tests written with UI Automator are so complex, non-readble and hard to maintain especially for testers. +Have a look at a typical piece of code written with UI Automator: +

val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+val uiDevice = UiDevice.getInstance(instrumentation)
+
+val uiObject = uiDevice.wait(
+    Until.findObject(
+        By.res(
+            "com.kaspersky.kaspresso.sample_kautomator",
+            "editText"
+        )
+    ),
+    2_000
+)
+
+uiObject.text = "Kaspresso"
+assertEquals(uiObject.text, "Kaspresso")
+
+This is an example just to input and check the text. Because we have a successful experience of Kakao using we decided to wrap UI Automator over in the same manner and called it Kautomator: +
MainScreen {
+    simpleEditText {
+        replaceText("Kaspresso")
+        hasText("Kaspresso")
+    }
+}
+

+

Another big advantage of Kautomator is a possibility to accelerate UI Automator.
+Have a glance at video below:

+


+The left video is boosted UI Automator, the right video is default UI Automator.

+

Why is it possible? The details are available a little bit later.

+

Benefits

+
    +
  • Readability
  • +
  • Reusability
  • +
  • Extensible DSL
  • +
  • Amazing speed!
  • +
+

How to use it

+

Create Screen

+

Create your entity UiScreen where you will add the views involved in the interactions of the tests: +

class FormScreen : UiScreen<FormScreen>()
+
+UiScreen can represent the whole user interface or a portion of UI. +If you are using Page Object pattern you can put the interactions of Kautomator inside the Page Objects.

+

Create UiView

+

UiScreen contains UiView, these are the Android Framework views where you want to do the interactions: +

class FormScreen : UiScreen<FormScreen>() {
+    val phone = UiView { withId(this@FormScreen.packageName, "phone") }
+    val email = UiEditText { withId(this@FormScreen.packageName, "email_edit") }
+    val submit = UiButton { withId(this@FormScreen.packageName, "submit_button") }
+}
+
+Kautomator provides different types depending on the type of view:

+
    +
  • UiView
  • +
  • UiEditText
  • +
  • UiTextView
  • +
  • UiButton
  • +
  • UiCheckbox
  • +
  • UiChipGroup
  • +
  • UiSwitchView
  • +
  • UiScrollView
  • +
  • and more
  • +
+

Every UiView contains matchers to retrieve the view involved in the ViewInteraction. Some examples of matchers provided +by Kakao:

+
    +
  • withId
  • +
  • withText
  • +
  • withPackage
  • +
  • withContentDescription
  • +
  • textStartsWith
  • +
  • and more
  • +
+

Like in Ui Automator you can combine different matchers: +

val email = UiEditText {
+    withId(this@FormScreen.packageName, "email")
+    withText(this@FormScreen.packageName, "matsyuk@kaspresso.com")
+}
+

+

Write the interaction

+

The syntax of the test with Kautomator is very easy, once you have the UiScreen and the UiView defined, you only have to apply +the actions or assertions like in UI Automator: +

FormScreen {
+    phone {
+       hasText("971201771")
+    }
+    button {
+       click()
+    }
+}
+

+

The difference from Kakao-Espresso

+

In Espresso, all interaction with a View is processing through ViewInteraction that has two main methods: +onCheck and onPerform which take ViewAction and ViewAssertion as arguments. Kakao was written based on this architecture.

+

So, we have set a goal to write Kautomator which would be like Kakao as much as possible. That's why we have introduced an additional layer over UiObject2 and UiDevice and that is so similar to ViewInteraction. This layer is represented by UiObjectInteraction and UiDeviceInteraction that have two methods: onCheck and onPerform taking UiObjectAssertion and UiObjectAction or UiDeviceAssertion and UiDeviceAction as arguments.

+

UiObjectInteraction is designed to work with concrete View like ViewInteraction. UiDeviceInteraction has been created because UI Automator has a featureallowing you to do some system things like a click on Home button or on hard Back button, open Quick Setttings, open Notifications and so on. All such things are hidden by UiSystem class.

+

So, enjoy it =)

+

Advanced

+
Custom UiView
+

If you have custom Views in your tests and you want to create your own UiView, we have UiBaseView. Just extend +this class and implement as much additional Action/Assertion interfaces as you want. +You also need to override constructors that you need.

+
class UiMyView : UiBaseView<KView>, UiMyActions, UiMyAssertions {
+    constructor(selector: UiViewSelector) : super(selector)
+    constructor(builder: UiViewBuilder.() -> Unit) : super(builder)
+}
+
+
Intercepting
+

If you need to add custom logic during the Kautomator -> UI Automator call chain (for example, logging) or +if you need to completely change the UiAssertion or UiAction that are being sent to UI Automator +during runtime in some cases, you can use the intercepting mechanism.

+

Interceptors are lambdas that you pass to a configuration DSL that will be invoked before real calls +inside UiObject2 and UiDevice classes in UI Automator.

+

You have the ability to provide interceptors at 3 different levels: Kautomator runtime, your UiScreen classes +and any individual UiView instance.

+

On each invocation of UI Automator function that can be intercepted, Kautomator will aggregate all available interceptors +for this particular call and invoke them in descending order: UiView interceptor -> Active Screens interceptors -> +Kautomator interceptor.

+

Each of the interceptors in the chain can break the chain call by setting isOverride to true during configuration. +In that case Kautomator will not only stop invoking remaining interceptors in the chain, but will not perform the UI Automator +call. It means that in such case, the responsibility to actually invoke Kautomator lies on the shoulders +of the developer.

+

Here's the examples of intercepting configurations: +

class SomeTest {
+    @Before
+    fun setup() {
+        KautomatorConfigurator { // Kautomator runtime
+            intercept {
+                onUiInteraction { // Intercepting calls on UiInteraction classes across whole runtime
+                    onPerform { uiInteraction, uiAction -> // Intercept perform() call
+                        testLogger.i("KautomatorIntercept", "interaction=$uiInteraction, action=$uiAction")
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun test() {
+        MyScreen {
+            intercept {
+                onUiInteraction { // Intercepting calls on UiInteraction classes while in the context of MyScreen
+                    onCheck { uiInteraction, uiAssert -> // Intercept check() call
+                        testLogger.i("KautomatorIntercept", "interaction=$uiInteraction, assert=$uiAssert")
+                    }
+                }
+            }
+
+            myView {
+                intercept { // Intercepting ViewInteraction calls on this individual view
+                    onPerform(true) { uiInteraction, uiAction -> // Intercept perform() call and overriding the chain
+                        // When performing actions on this view, Kautomator level interceptor will not be called
+                        // and we have to manually call UI Automator now.
+                        Log.d("KAUTOMATOR_VIEW", "$uiInteraction is performing $uiAction")
+                        uiInteraction.perform(uiAction)
+                    }
+                }
+            }
+        }
+    }
+}
+

+

Accelerate UI Automator

+

As you remember we told about the possible acceleration of UI Automator. How does it become a reality?
+UI Automator has an inner mechanism to prevent potential flakiness. Under the hood, the library listens and gives commands through AccessibilityManagerService. AccessibilityManagerService is a single point for all accessibility events in the system. At one moment, creators of UI Automator faced with the flakiness problem. One of the most popular reasons for such undetermined behavior is a big count of events processing in the System at the current moment. But UI Automator has a connection with AccessibilityManagerService. Such a connection gives an opportunity to listen to all accessibility events in the System and to wait for a calm state when there are no actions. The calm state leads to determined system behavior and decreases the possibility of flakiness.
+All of this pushed UI Automator authors to introduce the following algorithm: UI Automator waits 500ms (waitForIdleTimeout and waitForSelectorTimeout in androidx.test.uiautomator.Configurator) window during 10 seconds for each action. EACH ACTION.

+

Perhaps, described solution made UI Automator more stable. But, the speed crashed, no doubts.

+

Kautomator is a DSL over UI Automator that provides a mechanism of interceptors. Kaspresso offers a big set of default interceptors which eliminates any potential flaky action. So, Kaspresso + Kautomator helps UI Automator to struggle with flakiness.

+

After some time, we thought why we need to save artificial timeouts inside UI Automator while Kaspresso + Kautomator does the same work. Have a look at the measure example: +

@RunWith(AndroidJUnit4::class)
+class KautomatorMeasureTest : TestCase(
+    kaspressoBuilder = Kaspresso.Builder.simple {
+        kautomatorWaitForIdleSettings = KautomatorWaitForIdleSettings.boost()
+    }
+) {
+
+    companion object {
+        private val RANGE = 0..20
+    }
+
+    @get:Rule
+    val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        Manifest.permission.WRITE_EXTERNAL_STORAGE,
+        Manifest.permission.READ_EXTERNAL_STORAGE
+    )
+
+    @get:Rule
+    val activityTestRule = ActivityTestRule(MainActivity::class.java, true, false)
+
+    @Test
+    fun test() =
+        before {
+            activityTestRule.launchActivity(null)
+        }.after { }.run {
+
+    ======> UI Automator:        0 minutes, 1 seconds and 252 millis
+    ======> UI Automator boost:  0 minutes, 0 seconds and 310 millis
+            step("MainScreen. Click on `measure fragment` button") {
+                UiMainScreen {
+                    measureButton {
+                        isDisplayed()
+                        click()
+                    }
+                }
+            }
+
+    ======> UI Automator:        0 minutes, 11 seconds and 725 millis
+    ======> UI Automator boost:  0 minutes, 1 seconds and 50 millis
+            step("Measure screen. Button_1 clicks comparing") {
+                UiMeasureScreen {
+                    RANGE.forEach { _ ->
+                        button1 {
+                            click()
+                            hasText(device.targetContext.getString(R.string.measure_fragment_text_button_1).toUpperCase())
+                        }
+                    }
+                }
+            }
+
+    ======> UI Automator:        0 minutes, 11 seconds and 789 millis
+    ======> UI Automator boost:  0 minutes, 1 seconds and 482 millis
+            step("Measure screen. Button_2 clicks and TextView changes comparing") {
+                UiMeasureScreen {
+                    RANGE.forEach { index ->
+                        button2 {
+                            click()
+                            hasText(device.targetContext.getString(R.string.measure_fragment_text_button_2).toUpperCase())
+                        }
+                        textView {
+                            hasText(
+                                "${device.targetContext.getString(R.string.measure_fragment_text_textview)}${index + 1}"
+                            )
+                        }
+                    }
+                }
+            }
+
+    ======> UI Automator:        0 minutes, 45 seconds and 903 millis
+    ======> UI Automator boost:  0 minutes, 2 seconds and 967 millis
+            step("Measure fragment. EditText updates comparing") {
+                UiMeasureScreen {
+                    edit {
+                        isDisplayed()
+                        hasText(device.targetContext.getString(R.string.measure_fragment_text_edittext))
+                        RANGE.forEach { _ ->
+                            clearText()
+                            typeText("bla-bla-bla")
+                            hasText("bla-bla-bla")
+                            clearText()
+                            typeText("mo-mo-mo")
+                            hasText("mo-mo-mo")
+                            clearText()
+                        }
+                    }
+                }
+            }
+
+    ======> UI Automator:        0 minutes, 10 seconds and 901 millis
+    ======> UI Automator boost:  0 minutes, 1 seconds and 23 millis
+            step("Measure fragment. Checkbox clicks comparing") {
+                UiMeasureScreen {
+                    RANGE.forEach { index ->
+                        checkBox {
+                            if (index % 2 == 0) {
+                                setChecked(true)
+                                isChecked()
+                            } else {
+                                setChecked(false)
+                                isNotChecked()
+                            }
+                        }
+                    }
+                }
+            }
+        }
+}
+
+It's a great deal!

+

Also, there are cases when UI Automator can't catch 500ms window. For example, when one element is updating too fast (one update in 100 ms). Just have a look at this test. Only KautomatorWaitForIdleSettings.boost() allows to pass the test.

+

As you see, we have introduced a special kautomatorWaitForIdleSettings property in Kaspresso configurator. By default, this property is not boost. Why? Because: +1. You can have tests where you use UI Automator directly. But mentioned timeouts are global parameters. Resetting of these timeouts can lead to an undetermined state. +2. We want to take time collecting data from the world and then to analyze potential problems of our solutions (but, we believe it's a stable and brilliant solution).

+

Another important remark is about kaspressoBuilder = Kaspresso.Builder.simple configuration. This configuration is faster than advanced because of each step's screenshots interceptor absence. If you need, add them manually.

+

Anyway, it's a small change for a developer, but it's a big step for the world =)

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Matchers_actions_assertions/index.html b/Wiki/Matchers_actions_assertions/index.html new file mode 100644 index 000000000..d6535974b --- /dev/null +++ b/Wiki/Matchers_actions_assertions/index.html @@ -0,0 +1,1368 @@ + + + + + + + + + + + + + + + + + + + + + + + + + View matchers, actions and assertions - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Matchers, Actions and Assertions

+

As you all know Kaspresso is based on Espresso (if you're not familiar with Espresso, check out the official docs). +
According to official docs the main components of Espresso include the following:

+
    +
  1. Espresso – Entry point to interactions with views (via onView() and onData()). Also exposes APIs that are not necessarily tied to any view, such as pressBack().
  2. +
  3. ViewMatchers – A collection of objects that implement the Matcher<? super View> interface. You can pass one or more of these to the onView() method to locate a view within the current view hierarchy.
  4. +
  5. ViewActions – A collection of ViewAction objects that can be passed to the ViewInteraction.perform() method, such as click().
  6. +
  7. ViewAssertions – A collection of ViewAssertion objects that can be passed the ViewInteraction.check() method. Most of the time, you will use the matches assertion, which uses a View matcher to assert the state of the currently selected view.
  8. +
+
// withId(R.id.my_view) is a ViewMatcher
+// click() is a ViewAction
+// matches(isDisplayed()) is a ViewAssertion
+onView(withId(R.id.my_view))
+    .perform(click())
+    .check(matches(isDisplayed()))
+
+

Most available instances of Matcher, ViewActions and ViewAssertions can be found in the Google cheat-sheet. +Espresso cheat sheet

+

The results of calling onView() methods (ViewInteractors) can be cashed. In Kakao you can get references to ViewInteractors and reuse them in your code. This makes your code in tests more readable and understandable. +
This framework also allows you to separate the search for an element and actions on it. Kakao has introduced KView and various implementations for the most available Android widgets. This KView implements the BaseAssertions and BaseActions interfaces with some additional methods. Every inheritor of KView implements its own interfaces for assertions and actions for some widget-specific methods. +
As a result, you can get a reference to specific views from your test code and make the necessary assertions and actions on it in the view block.

+


Since Kasresso inherits all the best from these two frameworks, everything described above is available to you.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Page_object_in_Kaspresso/index.html b/Wiki/Page_object_in_Kaspresso/index.html new file mode 100644 index 000000000..60e775414 --- /dev/null +++ b/Wiki/Page_object_in_Kaspresso/index.html @@ -0,0 +1,1523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + PageObject in Kaspresso - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Page object pattern in Kaspresso.

+

What is a Page object pattern?

+


Page object pattern is explained well by Martin Fowler in this article. Long in short this is a test abstraction that describes the screen with some view elements. These view items can be interacted with during tests. As a result the description of the screen elements will be in a separate class. You no longer need to constantly look for the desired UI element with several matchers in tests. This can be done once by saving a link to the screen.

+

How is the page object pattern implemented in Kaspresso?

+


Kaspresso provides KScreen and UiScreen as implementations for Page object pattern.

+

What is the difference between KScreen and UiScreen

+


Kaspresso is based on Kakao and UiAutomator. +
When we have all info about the application code(white-box testing cases) we should use KScreen to describe the structure of PageObject as Kakao does. This is a class in Kaspresso - extension for Kakao Screen class. +
When we don't have access to a source code of an application (it can be some system dialogs, windows or apps) we should use UiScreen. +
Here are two samples: +

object SimpleScreen : KScreen<SimpleScreen>() {
+
+    override val layoutId: Int? = R.layout.activity_simple
+    override val viewClass: Class<*>? = SimpleActivity::class.java
+
+    val button1 = KButton { withId(R.id.button_1) }
+
+    val button2 = KButton { withId(R.id.button_2) }
+
+    val edit = KEditText { withId(R.id.edit) }
+}
+
+object MainScreen : UiScreen<MainScreen>() {
+
+    override val packageName: String = "com.kaspersky.kaspresso.kautomatorsample"
+
+    val simpleEditText = UiEditText { withId(this@MainScreen.packageName, "editText") }
+    val simpleButton = UiButton { withId(this@MainScreen.packageName, "button") }
+    val checkBox = UiCheckBox { withId(this@MainScreen.packageName, "checkBox") }
+}
+
+
In KScreen's inheritors we should initialize the layoutId (layout file of a screen) and viewClass(screen activity class name) fields. But this is optional. These fields will help in cases of code refactoring not to forget about the associated tests screens +
In UiScreen's inheritors we must initialize packageName field (the full name of the application's package).

+

Benefits of the page object for refactoring

+


Page object pattern allows you to exclude the description of the screen in a separate file and to reuse Screens and views in different tests. When you have some changes in the UI of the application you can only change the code in the Screen file without the need for a lot of refactoring of the tests.

+

Benefits of the Page Object for a work in a team

+


In some teams autotests are written only by developers, in others by QA engineers. In some cases autotests are written by someone who does not know details of the code (source code is available, but is bad understandable). In this case developers can write Screens for additional autotests. Having Screens helps another person to write tests using Kotlin DSL.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Screenshot_tests/index.html b/Wiki/Screenshot_tests/index.html new file mode 100644 index 000000000..0c4f8218d --- /dev/null +++ b/Wiki/Screenshot_tests/index.html @@ -0,0 +1,1679 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Screenshot tests - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Screenshot tests

+

Main purpose

+

Sometimes when developing new features, there is a need to check if the application works properly in all supported languages. Manual locale setting changes could take a long time and require the efforts of developers, QA engineers, and etc. Also, it could increase the duration of the localization process.

+

In order to avoid that, Kaspresso provides DocLocScreenshotTestCase +which allows taking screenshots in all locales you specified. DocLocScreenshotTestCase extends +default Kaspresso TestCase and offers the opportunity to make screenshots out the box by +calling DocLocScreenshotTestCase#captureScreenshot(String) method.

+

Usage

+

To create a single test, you should extend DocLocScreenshotTestCase class as shown below:

+
@RunWith(AndroidJUnit4::class)
+class ScreenshotSampleTest : DocLocScreenshotTestCase(
+    locales = "en,ru"
+) {
+
+    @ScreenShooterTest
+    @Test
+    fun test() {
+        before{
+        }.after {
+        }.run {
+
+            step("1. Do the first step") {
+                // ...
+                captureScreenshot("First step")
+            } 
+
+            step("2. Do the second step") {
+                // ... 
+                captureScreenshot("Second step")
+            }
+        }
+    }        
+}
+
+

There is one parameter passed in the base constructor: +- locales - comma-separated string with locales to run test with. + Captured screenshots will be available in the device's storage at the path "/sdcard/documents/screenshots/".

+

For full example, check the ScreenshotSampleTest.

+

Notice, that the test is marked with @ScreenShooterTest annotation. This is intended to filter only screenshooter tests to be run. For example, you could pass the +annotation to default AndroidJUnitRunner with command:

+
adb shell am instrument -w -e annotation com.kaspersky.kaspresso.annotations.ScreenShooterTest your.package.name/android.support.test.runner.AndroidJUnitRunner
+
+

Screenshot files location

+

All screenshot files are stored in "screenshots" directory by default. +They are sorted by locale and test name:

+

<base directory>/<test class canonical name>/<locale>/<your tag>.png

+

For the sample test case, the files tree should be like:

+
- screenshots
+    -  com.kaspersky.kaspressample.tests.docloc.ScreenshotSampleTest
+        - en
+            // screenshot files
+        - ru
+            // screenshot files
+
+

So, in order to save screenshots at external storage, the test application requires +android.permission.WRITE_EXTERNAL_STORAGE permission.

+

Screenshot's additional meta-info

+

When a developer calls captureScreenshot("la-la-la") method then Kaspresso creates not only a screenshot but also a special xml file. This xml file contains data about all ui elements with their id located on the screen. Example: +

<Metadata>
+    <Window Left="0" Top="0" Width="1440" Height="2560">
+        <LocalizedString Text="Simple Fragment" LocValueDescription="com.kaspersky.kaspressample.test:id/text_view_title" Top="140" Left="307" Width="825" Height="146"/>
+        <LocalizedString Text="Button 1" LocValueDescription="com.kaspersky.kaspressample.test:id/button_1" Top="370" Left="84" Width="1272" Height="168"/>
+        <LocalizedString Text="Button 2" LocValueDescription="com.kaspersky.kaspressample.test:id/button_2" Top="622" Left="84" Width="1272" Height="168"/>
+        <LocalizedString Text="Kaspresso" LocValueDescription="com.kaspersky.kaspressample.test:id/edit" Top="874" Left="84" Width="1272" Height="158"/>
+        <LocalizedString Text="Simple screen" LocValueDescription="com.kaspersky.kaspressample.test:id/[id:ffffffff]" Top="51" Left="56" Width="446" Height="93"/>
+    </Window>
+</Metadata>
+
+Similar data may be useful for different systems automating the process of localization of an application. The automating system saves xml for each screen and compares it with new versions received by new screenshot's runs. If some difference were revealed the system gives a signal to prepare and send a portion of new words to translate server.

+

Screenshots of system dialogs/windows

+

Sometimes you want to take screenshots of Android system dialogs or windows. That's why you have to change the language for the entire system. For this purpose, there is additional param in DocLocScreenshotTestCase constructor - changeSystemLocale. Pay your attention to the fact that changeSystemLocale defined in true demands Manifest.permission.CHANGE_CONFIGURATION.
+Have a look at the code below: +

@RunWith(AndroidJUnit4::class)
+class ChangeSysLanguageTestCase : DocLocScreenshotTestCase(
+    screenshotsDirectory = File("screenshots"),
+    locales = "en,ru",
+    changeSystemLocale = true
+) {
+
+    @ScreenShooterTest
+    @Test
+    fun test() {
+        before{
+        }.after {
+        }.run {
+
+            step("1. Do the first step") {
+                // ...
+                captureScreenshot("First step")
+            } 
+
+            step("2. Do the second step") {
+                // ... 
+                captureScreenshot("Second step")
+            }
+        }
+    }        
+}
+
+The full example is located at ChangeSysLanguageTestCase.

+

Important note

+

Please keep the strategy "one docloc test == one screen". If you will seek to capture screenshots from more than one screen during one test consequences may be unpredictable. Be aware.

+

Advanced usage

+

In most cases, there is no need to launch certain activity, do a lot of steps before reaching necessary functionality. Often showing fragments will be sufficient to make required screenshots. +Also, when you use Model-View-Presenter architectural pattern, you are able to control UI state +directly through the View interface. So, there is no need to interact with the application interface and wait for changes.

+

First create a base test activity with setFragment(Fragment) method in your application:

+
class FragmentTestActivity : AppCompatActivity() {
+
+    fun setFragment(fragment: Fragment) = with(supportFragmentManager.beginTransaction()) {
+        replace(android.R.id.content, fragment)
+        commit()
+    }
+}
+
+

Then add a base product screenshot test case:

+

```kotlin +open class ProductDocLocScreenshotTestCase : DocLocScreenshotTestCase( + locales = "en,ru" +) {

+
@get:Rule
+val activityTestRule = ActivityTestRule(FragmentTestActivity::class.java, false, true)
+
+protected val activity: FragmentTestActivity
+    get() = activityTestRule.activity
+
+

} +

This test case would run your `FragmentTestActivity` on startup. Now you are able to write your screenshooter tests.
+For example, create a new test class which extends `ProductDocLocScreenshotTestCase`:
+
+```kotlin
+@RunWith(AndroidJUnit4::class)
+class AdvancedScreenshotSampleTest : ProductDocLocScreenshotTestCase() {
+
+    private lateinit var fragment: FeatureFragment
+    private lateinit var view: FeatureView
+
+    @ScreenShooterTest
+    @Test
+    fun test() {
+        before {
+            fragment = FeatureFragment()
+            view = getUiSafeProxy(fragment as FeatureView)
+            activity.setFragment(fragment)
+        }.after {
+        }.run {
+
+            step("1. Step 1") {
+                // ... [view] calls
+                captureScreenshot("Step 1")
+            }
+
+            step("2. Step 2") {
+                // ... [view] calls
+                captureScreenshot("Step 2")
+            }
+
+            step("3. Step 3") {
+                // ... [view] calls
+                captureScreenshot("Step 3")
+            }
+
+            // ... other steps
+        }
+    }
+}
+

+

As you might notice, the getUiSafeProxy method called to get an instance of FeatureView. +This method wraps your View interface and returns a proxy on it. +The proxy guarantees that all the methods of the View interface you called, will be invoked on the main thread. +There is also getUiSafeProxyFromImplementation which wraps an implementation rather than an interface.

+

For full example, check AdvancedScreenshotSampleTest class.

+

Modifying screenshots path and name

+

By default, all screenshots are stored at:
+/sdcard/documents/screenshots/<locale>/<full qualified test class name>/<method name>.
+You can change this behavior by providing custom +ResourcesRootDirsProvider, +ResourcesDirsProvider, +ResourceFileNamesProvider and +ResourcesDirNameProvider implementations.

+

Find out details here.

+

Changes

+

We have been forced to redesign our resource providing system to support Allure. +That's why we changed the primary constructor of DocLocScreenshotTestCase. +But, we've kept the old option of using DocLocScreenshotTestCase with old resource providing system as a secondary constructor. +You can view the secondary constructor as an example of migration from old system to new system. +Also, we've retained tests using old resource providing system in samples to ensure that nothing is broken.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Supported_Android_UI_elements/index.html b/Wiki/Supported_Android_UI_elements/index.html new file mode 100644 index 000000000..15d03030a --- /dev/null +++ b/Wiki/Supported_Android_UI_elements/index.html @@ -0,0 +1,1464 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Supported Android UI-elements - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Supported Android UI widgets

+

Via Kakao

+

All the supported Android UI widgets in Kakao can be found as inheritors of the KBaseView class. +
Here are some of them: +
KBottomNavigationView +
KCheckBox +
KChipGroup +
KSwipeView +
KView +
KAlertDialog +
KDrawerView +
KEditText +
KTextInputLayout +
KImageView +
KNavigationView +
KViewPager +
KDatePicker +
KDatePickerDialog +
KTimePicker +
KTimePickerDialog +
KProgressBar +
KSeekBar +
KRatingBar +
KScrollView +
KSearchView +
KSlider +
KSwipeRefreshLayout +
KSwitch +
KTabLayout +
KButton +
KSnackbar +
KTextView +
KToolbar

+

Via KAutomator

+

If you extend the UiScreen abstract class then the following views are available for you: +
UiView +
UiEditText +
UiTextView +
UiButton +
UiCheckbox +
UiChipGroup +
UiSwitchView +
UiScrollView +
UiBottomNavigationView

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/Working_with_Android_OS/index.html b/Wiki/Working_with_Android_OS/index.html new file mode 100644 index 000000000..f740d360f --- /dev/null +++ b/Wiki/Working_with_Android_OS/index.html @@ -0,0 +1,1489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Working with Android OS - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Working with Android OS. Kaspresso Device abstraction.

+

Device is a provider of managers for all off-screen work.

+

Structure

+

All examples are located in device_tests. +Device provides these managers:

+
    +
  1. apps allows to install or uninstall applications. Uses adb install and adb uninstall commands. See the example DeviceAppSampleTest.
  2. +
  3. activities is an interface to work with currently resumed Activities. AdbServer not required. See the example DeviceActivitiesSampleTest.
  4. +
  5. files provides the possibility of pushing or removing files from the device. Uses adb push and adb rm commands and does not require android.permission.WRITE_EXTERNAL_STORAGE permission. See the example DeviceFilesSampleTest.
  6. +
  7. internet allows toggling WiFi and network data transfer settings. Be careful of using this interface, WiFi settings changes could not work with some Android versions. See the example DeviceNetworkSampleTest.
  8. +
  9. keyboard is an interface to send key events via adb. Use it only when Espresso or UiAutomator are not appropriate (e.g. screen is locked). See the example DeviceKeyboardSampleTest.
  10. +
  11. location emulates fake location and allows to toggle GPS setting. See the example DeviceLocationSampleTest.
  12. +
  13. phone allows to emulate incoming calls and receive SMS messages. Works only on emulators since uses adb emu commands. See the example DevicePhoneSampleTest.
  14. +
  15. screenshots is an interface screenshots of currently resumed activity. Requires android.permission.WRITE_EXTERNAL_STORAGE permission. See the example DeviceScreenshotSampleTest.
  16. +
  17. accessibility allows to enable or disable accessibility services. Available since api 24. See the example DeviceAccessibilitySampleTest.
  18. +
  19. permissions provides the possibility of allowing or denying permission requests via default Android permission dialog. See the example DevicePermissionsSampleTest.
  20. +
  21. hackPermissions provides the possibility of allowing any permission requests without default Android permission dialog. See the example DeviceHackPermissionsSampleTest.
  22. +
  23. exploit allows to rotate device or press system buttons. See the example DeviceExploitSampleTest.
  24. +
  25. language allows to switch language. See the example DeviceLanguageSampleTest.
  26. +
  27. logcat provides access to adb logcat. See the example DeviceLogcatSampleTest.
    + The purpose of logcat:
    + If you have not heard about GDPR and high-profile lawsuits then you are lucky. But, if your application works in Europe then it's so important to enable/disable all analytics/statistics according to acceptance of the agreements. + One of the most reliable ways to check analytics/statistics sending is to verify logcat where all analytics/statistics write their logs (in debug mode, sure). + That's why we have created a special Logcat class providing a wide variety of ways to check logcat.
  28. +
  29. uiDevice returns an instance of android.support.test.uiautomator.UiDevice. We don't recommend to use it directly because there is Kautomator that offers a more readable, predictable and stable API to work outside your application.
  30. +
+

Also Device provides application and test contexts - targetContext and context.

+

Usage

+

Device instance is available in BaseTestContext scope and BaseTestCase via device property. +

@Test
+fun test() =
+    run {
+        step("Open Simple Screen") {
+            activityTestRule.launchActivity(null)
+  ======>   device.screenshots.take("Additional_screenshot")  <======
+
+            MainScreen {
+                simpleButton {
+                    isVisible()
+                    click()
+                }
+            }
+        }
+        // ....
+}
+

+

Restrictions

+

Most of the features that Device provides use of adb commands and requires AdbServer to be run. +Some of them, such as call emulation or SMS receiving, could be executed only on emulator. All such methods are marked by annotation @RequiresAdbServer.

+

All the methods which use ADB commands require android.permission.INTERNET permission. +For more information, see AdbServer documentation.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/how_to_write_autotests/index.html b/Wiki/how_to_write_autotests/index.html new file mode 100644 index 000000000..7bf349311 --- /dev/null +++ b/Wiki/how_to_write_autotests/index.html @@ -0,0 +1,1897 @@ + + + + + + + + + + + + + + + + + + + + + How to write autotests - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

How to write autotests

+

Anyone who starts to write UI-tests is facing with a problem of how to write UI-tests correctly. +At the beginning of our great way, we had three absolutely different UI-test code styles from four developers. It was amazing. +At that moment, we decided to do something to prevent it.
+That's why we have created rules on how to write UI-tests and we have tried to make Kaspresso helping to follow these rules. All rules are divided into two groups: abstractions and structure. Also, we have added a third part containing convenient things resolving the most common problems.

+

Abstractions

+

How many abstractions can you have in your tests?

+

Only one! It's a page object (PO), the term explained well by Martin Fowler in this article.
+In Kakao a Screen class (in Kautomator a UiScreen) is the implementation of PO. Each screen visible by the user even a simple dialog is a separate PO.
+Yes, there are cases when you need new abstraction and it's ok. But our advice is to think well before you introduce new abstraction.

+

How to determine whether View (fragment, dialog, anything) in the project has its description in some Kakao Screen?

+

In a big project with a lot of UI-tests, it's not an easy challenge. +That's why we have implemented an extended version of the Kakao Screen - KScreen (KScreen). In KScreen you have to implement two properties: layoutId and viewClass. So your search if the View has its description in some Kakao Screen becomes easier.
+In Kautomator, there is general UiScreen(UiScreen) that has an obligatory field - packageName.

+

Is it ok that your PO contains helper methods?

+

If these methods help to understand what the test is doing then it's ok.
+For example, compare two parts of code: +

MainScreen {
+    shieldView {
+        click()
+    }
+}
+
+and +
MainScreen {
+    navigateToTasksScreen()
+}
+
+object MainScreen : KScreen<MainScreen>() {
+    //...
+    fun navigateToTasksScreen() {
+        shieldView {
+            click()
+        }
+    }
+    //...
+}
+
+I am sure that method navigateToTasksScreen() is more "talking" than the simple click on some shieldView.

+

Can Screen contain inner state or logic?

+

No! PO doesn't have any inner state or logic. It's only a description of the UI of concrete View.

+

Assert help methods inside of PO. Is it ok?

+

We think it's ok because it simplifies the code and puts all info that is about Screen into one class. +The chosen approach doesn't lead to an uncontrolled grow of class size because even a dialog is a separate Screen, so we don't have a huge Screen describing half of all UI in the app.
+Just compare three parts of code executing the same thing: +

ReportsScreen {
+    assertQuarantinedDetectsCountAfterScan(0)
+}
+
+
ReportsScreen {
+    reportsListView {
+        childAt<ReportsScreen.ReportsItem>(1) {
+            body {
+                containsText("Detected: 0")
+                containsText("Quarantined: 0")
+                containsText("Deleted: 0")
+            }
+        }
+    }
+}
+
+
ReportsScreen {
+    val detectsCount = getDetectsCountAfterScan()
+    ReportsScreenAssertions.assertQuarantinedDetectsCountAfterScan(
+        detectsCount
+    )
+}
+
+We prefer the first variant. But we follow the next naming convention of such methods: assert<YourCheckName>.

+

Test structure

+

Test and Test-case correlation

+

First of all, let's consider the above-mentioned terms.
+Test-case is a scenario written in human language by a tester to check some feature.
+Test is an implementation of Test-case written in program language by developer/autotester.
+Terms were learned. Let's observe some test: +

@Test
+fun test() {
+    MainScreen {
+        nextButton {
+            isVisible()
+            click()
+        }
+    }
+    SimpleScreen {
+        button1 {
+            click()
+        }
+        button2 {
+            isVisible()
+        }
+    }
+    SimpleScreen {
+        button2 {
+            click()
+        }
+        edit {
+            attempt(timeoutMs = 7000) { isVisible() }
+            hasText(R.string.text_edit_text)
+        }
+    }
+}
+
+Not bad. But can you correlate this code with the test-case easy? +No, you need to read the code of the test and the text of the test-case very attentively. It's not comfortable.
+So we want to have a structure of the test that would suggest what step of the test-case we are looking at in the particular area of the test.

+

Before/after state of a test

+

Sometimes you have to change the state of a device (edit contacts, phones, put files into storage and more) while you are running a test.
+What to do with a changed state? There are two variants: +1. Create a universal method that sets a device to a consistent state. +2. Clean the state after each test.

+

The first approach doesn't look like a safe case because you need to remember about all the tests in one huge method.
+That's why we prefer the second approach. But it would be nice if the structure of a test forced us to remember about a state.

+

Test structure

+

All of the above mentioned inspired us to create the test's structure like below: +

@Test
+fun shouldPassOnNoInternetScanTest() =
+    before {
+        activityTestRule.launchActivity(null)
+        // some things with the state
+    }.after {
+        // some things with the state
+    }.run {
+        step("Open Simple Screen") {
+            MainScreen {
+                nextButton {
+                    isVisible()
+                    click()
+                }
+            }
+        }
+
+        step("Click button_1 and check button_2") {
+            SimpleScreen {
+                button1 {
+                    click()
+                }
+                button2 {
+                    isVisible()
+                }
+            }
+        }
+
+        step("Click button_2 and check edit") {
+            SimpleScreen {
+                button2 {
+                    click()
+                }
+                edit {
+                    attempt(timeoutMs = 7000) { isVisible() }
+                    hasText(R.string.text_edit_text)
+                }
+            }
+        }
+
+        step("Check all possibilities of edit") {
+            scenario(
+                CheckEditScenario()
+            )
+        }
+    }
+
+Let's describe the structure:
+1. before - after - run
+ In the beginning, we think about a state. After the state, we begin to consider the test body. +2. step
+ step in the test is similar to step in the test-case. That's why test reading is easier and understandable. +3. scenario
+ There are cases when some sentences of steps are absolutely identical and occur very often in tests. + For these sentences we have introduced a scenario where you can replace your sequences of steps.

+

How is this API enabled?
+Let's look at SimpleTest and +SimpleTestWithRule.
+In the first example we inherit SimpleTest from TestCase. In the second example we use TestCaseRule field. +Also you can use BaseTestCase and BaseTestCaseRule.

+

Test data for the test

+

A developer, while he is writing a test, needs to prepare some data for the test. It's a common case. Where do you locate test data preparing? +Usually, it's the beginning of the test.
+But, first, we want to divide test data preparing and test data usage. Second, we want to guarantee that test data were prepared before the test. +That's why we decided to introduce a special DSL to help and to highlight the work with test data preparing.
+Please look at the example - InitTransformDataTest.
+Updated DSL looks like: +

before {
+    // ...
+}.after {
+   // ...
+}.init {
+    company {
+        name = "Microsoft"
+        city = "Redmond"
+        country = "USA"
+    }
+    company {
+        name = "Google"
+        city = "Mountain View"
+        country = "USA"
+    }
+    owner {
+        firstName = "Satya"
+        secondName = "Nadella"
+        country = "India"
+    }
+    owner {
+        firstName = "Sundar"
+        secondName = "Pichai"
+        country = "India"
+    }
+}.transform {
+    makeOwner(ownerSurname = "Nadella", companyName = "Microsoft")
+    makeOwner(ownerSurname = "Pichai", companyName = "Google")
+}.run {
+    // ...
+}
+
+1. init
+ Here, you prepare only sets of data without any transforms and connections. Also, you can make requests to your test server, for example.
+ It's an optional block. +2. transform
+ This construction is for transforming of our test data. In our example we join the owner and company.
+ It's an optional block. The block is enabled only after the init block.

+

Alexander Blinov wrote a good article about init-transform DSL in russian article where he explains all DSL details very well. You are welcome!

+

Available Test DSL forms

+

Finally, let's look at all available Test DSL in Kaspresso: +1. before-after-init-transform-run +1. before-after-init-transform-transform-run. It's possible to add multiple transform blocks. +2. before-after-init-run +3. before-after-run +4. init-transform-run +5. init-transform-transform-run. It's possible to add multiple transform blocks. +6. init-run +7. run

+

Examples

+

You can have a look at examples of how to use and configure Kaspresso +and how to use different forms of DSL.

+

Sweet additional features

+

Some words about BaseTestContext method

+

You can notice an existing of some BaseTestContext in before, after and run methods. BaseTestContext gives you access to all Kaspresso's entities that a developer can need during the test. Also, BaseTestContext gives you insurance that all of these entities were created correctly for the current session and with actual Kaspresso configurator.
+So, let's consider what BaseTestContext offers.

+

flakySafely

+

It's a method that receives a lambda and invokes it in the same manner as FlakySafeInterceptors group.
+If you disabled this interceptor or if you want to set some special flaky safety params for any view, you can use this method. The most common case is when the default timeout (10 seconds) for flakySafety is not enough, because, for example, the appearance of a view is blocked by long background operation. +

step("Check tv6's text") {
+    CommonFlakyScreen {
+        tv6 {
+            flakySafely(timeoutMs = 16_000) {
+                hasText(R.string.common_flaky_final_textview)
+            }
+        }
+    }
+}
+
+More detailed examples are here. Please, observe a documentation about implementation details.

+

continuously

+

This function is similar to what flakySafely does, but for negative scenarios, where you need all the time to check that something does not happen. +

ContinuouslyDialogScreen {
+    continuously() {
+        dialogTitle {
+            doesNotExist()
+        }
+    }
+}
+
+The example is here.

+

compose

+

This is a method to make a composed action from multiple actions or assertions, and this action succeeds if at least one of its components succeeds. +compose is useful in cases when we don't know an accurate sequence of events and can't influence it. Such cases are possible when a test is performed outside the application. +When a test is performed inside the application we strongly recommend to make your test linear and don't put any conditions in tests that are possible thanks to compose.
+It is available as an extension function for any KView, UiBaseView and as just a regular method (in this case it can take actions on different views as well).

+

The key words using in compose: +- compose - marks the beginning of "compose", turn on all needed logic +- or - marks the possible branches. The lambda after or has a context of concrete element. Just have a look at the simple below. +- thenContinue - is an action that will be executed if a branch (the code into lambda of or) is completed successfully. The context of a lambda after thenContinue is a context of concrete element described in or section. +- then - is almost the same construction as thenContinue excepting the context after then. The context after then is not restricted.

+

Have a glance at the example below: +

step("Handle potential unexpected behavior") {
+    // simple compose
+    CommonFlakyScreen {
+        btn5.compose {
+            or {
+                // the context of this lambda is `btn5`
+                hasText("Something wrong")
+            } thenContinue {
+                // here, the context of this lambda is a context of KButton(btn5),
+                // that's why we can call KButton's methods inside the lambda directly
+                click()
+            }
+            or {
+                // the context of this lambda is `btn5`
+                hasText(R.string.common_flaky_final_button)
+            } then {
+                // here, there is not any special context of this lambda
+                // that's why we can't call KButton's methods inside the lambda directly
+                btn5.click()
+            }
+        }
+    }
+    // complex compose
+    compose {
+        // the first potential branch when ComplexComposeScreen.stage1Button is visible
+        or(ComplexComposeScreen.stage1Button) {
+            // the context of this lambda is `ComplexComposeScreen.stage1Button`
+            isVisible()
+        } then {
+            // if the first branch was succeed then we execute some special flow
+            step("Flow is over the product") {
+                ComplexComposeScreen {
+                    stage1Button {
+                        click()
+                    }
+                    stage2Button {
+                        isVisible()
+                        click()
+                    }
+                }
+            }
+        }
+        // the second potential branch when UiComposeDialog1.title is visible
+        // just imagine that is some unexpected system or product behavior and we cannot fix it now
+        or(UiComposeDialog1.title) {
+            // the context of this lambda is `UiComposeDialog1.title`
+            isDisplayed()
+        } then {
+            // if the second branch was succeed then we execute some special flow
+            step("Flow is over dialogs") {
+                UiComposeDialog1 {
+                    okButton {
+                        isDisplayed()
+                        click()
+                    }
+                }
+                UiComposeDialog2 {
+                    title {
+                        isDisplayed()
+                    }
+                    okButton {
+                        isDisplayed()
+                        click()
+                    }
+                }
+            }
+        }
+    }
+}
+
+The example is here.
+Please, observe additional opportunities and documentation: common info, ComposeProvider and WebComposeProvider.

+

data

+

If you set your test data by init-transform methods then this test data is available by a data field.

+

testAssistants

+

Special assistants to write tests. Pay attention to the fact that these assistants are available in BaseTestCase also.
+1. testLogger
+ It's a logger for tests allowed to output logs by a more appropriate and readable form. +2. device
+ An instance of Device class is available in this context. It's a special interface given beautiful possibilities to do a lot of useful things at the test.
+ More detailed info about Device is here. +3. adbServer
+ You have access to AdbServer instance used in Device's interfaces via adbServer property.
+ More detailed info about AdbServer is here. +4. params
+ Params is the facade class for all Kaspresso parameters.
+ Please, observe the source code.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Wiki/index.html b/Wiki/index.html new file mode 100644 index 000000000..0a7ea7e66 --- /dev/null +++ b/Wiki/index.html @@ -0,0 +1,1348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Introduction - Kaspresso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Kaspresso Wiki

+

Here you can find detailed information about all the Kaspresso features.

+ + + + + + + + + + + + + +
+
+ + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 000000000..1cf13b9f9 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.fe8b6f2b.min.js b/assets/javascripts/bundle.fe8b6f2b.min.js new file mode 100644 index 000000000..cf778d428 --- /dev/null +++ b/assets/javascripts/bundle.fe8b6f2b.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Fi=Object.create;var gr=Object.defineProperty;var ji=Object.getOwnPropertyDescriptor;var Wi=Object.getOwnPropertyNames,Dt=Object.getOwnPropertySymbols,Ui=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty,no=Object.prototype.propertyIsEnumerable;var oo=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,R=(e,t)=>{for(var r in t||(t={}))xr.call(t,r)&&oo(e,r,t[r]);if(Dt)for(var r of Dt(t))no.call(t,r)&&oo(e,r,t[r]);return e};var io=(e,t)=>{var r={};for(var o in e)xr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Dt)for(var o of Dt(e))t.indexOf(o)<0&&no.call(e,o)&&(r[o]=e[o]);return r};var yr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Di=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Wi(t))!xr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=ji(t,n))||o.enumerable});return e};var Vt=(e,t,r)=>(r=e!=null?Fi(Ui(e)):{},Di(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var ao=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var co=yr((Er,so)=>{(function(e,t){typeof Er=="object"&&typeof so!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(H){return!!(H&&H!==document&&H.nodeName!=="HTML"&&H.nodeName!=="BODY"&&"classList"in H&&"contains"in H.classList)}function p(H){var mt=H.type,ze=H.tagName;return!!(ze==="INPUT"&&a[mt]&&!H.readOnly||ze==="TEXTAREA"&&!H.readOnly||H.isContentEditable)}function c(H){H.classList.contains("focus-visible")||(H.classList.add("focus-visible"),H.setAttribute("data-focus-visible-added",""))}function l(H){H.hasAttribute("data-focus-visible-added")&&(H.classList.remove("focus-visible"),H.removeAttribute("data-focus-visible-added"))}function f(H){H.metaKey||H.altKey||H.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(H){o=!1}function h(H){s(H.target)&&(o||p(H.target))&&c(H.target)}function w(H){s(H.target)&&(H.target.classList.contains("focus-visible")||H.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(H.target))}function A(H){document.visibilityState==="hidden"&&(n&&(o=!0),te())}function te(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function ie(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(H){H.target.nodeName&&H.target.nodeName.toLowerCase()==="html"||(o=!1,ie())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",A,!0),te(),r.addEventListener("focus",h,!0),r.addEventListener("blur",w,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var Yr=yr((Rt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Rt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Rt=="object"?Rt.ClipboardJS=r():t.ClipboardJS=r()})(Rt,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ii}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(_){return!1}}var h=function(_){var M=f()(_);return u("cut"),M},w=h;function A(V){var _=document.documentElement.getAttribute("dir")==="rtl",M=document.createElement("textarea");M.style.fontSize="12pt",M.style.border="0",M.style.padding="0",M.style.margin="0",M.style.position="absolute",M.style[_?"right":"left"]="-9999px";var j=window.pageYOffset||document.documentElement.scrollTop;return M.style.top="".concat(j,"px"),M.setAttribute("readonly",""),M.value=V,M}var te=function(_,M){var j=A(_);M.container.appendChild(j);var D=f()(j);return u("copy"),j.remove(),D},ie=function(_){var M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},j="";return typeof _=="string"?j=te(_,M):_ instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(_==null?void 0:_.type)?j=te(_.value,M):(j=f()(_),u("copy")),j},J=ie;function H(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?H=function(M){return typeof M}:H=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},H(V)}var mt=function(){var _=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=_.action,j=M===void 0?"copy":M,D=_.container,Y=_.target,ke=_.text;if(j!=="copy"&&j!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&H(Y)==="object"&&Y.nodeType===1){if(j==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(j==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(ke)return J(ke,{container:D});if(Y)return j==="cut"?w(Y):J(Y,{container:D})},ze=mt;function Ie(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(M){return typeof M}:Ie=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},Ie(V)}function _i(V,_){if(!(V instanceof _))throw new TypeError("Cannot call a class as a function")}function ro(V,_){for(var M=0;M<_.length;M++){var j=_[M];j.enumerable=j.enumerable||!1,j.configurable=!0,"value"in j&&(j.writable=!0),Object.defineProperty(V,j.key,j)}}function Ai(V,_,M){return _&&ro(V.prototype,_),M&&ro(V,M),V}function Ci(V,_){if(typeof _!="function"&&_!==null)throw new TypeError("Super expression must either be null or a function");V.prototype=Object.create(_&&_.prototype,{constructor:{value:V,writable:!0,configurable:!0}}),_&&br(V,_)}function br(V,_){return br=Object.setPrototypeOf||function(j,D){return j.__proto__=D,j},br(V,_)}function Hi(V){var _=Pi();return function(){var j=Wt(V),D;if(_){var Y=Wt(this).constructor;D=Reflect.construct(j,arguments,Y)}else D=j.apply(this,arguments);return ki(this,D)}}function ki(V,_){return _&&(Ie(_)==="object"||typeof _=="function")?_:$i(V)}function $i(V){if(V===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return V}function Pi(){if(typeof Reflect=="undefined"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(V){return!1}}function Wt(V){return Wt=Object.setPrototypeOf?Object.getPrototypeOf:function(M){return M.__proto__||Object.getPrototypeOf(M)},Wt(V)}function vr(V,_){var M="data-clipboard-".concat(V);if(_.hasAttribute(M))return _.getAttribute(M)}var Ri=function(V){Ci(M,V);var _=Hi(M);function M(j,D){var Y;return _i(this,M),Y=_.call(this),Y.resolveOptions(D),Y.listenClick(j),Y}return Ai(M,[{key:"resolveOptions",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Ie(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function(ke){return Y.onClick(ke)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,ke=this.action(Y)||"copy",Ut=ze({action:ke,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Ut?"success":"error",{action:ke,text:Ut,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return w(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,ke=!!document.queryCommandSupported;return Y.forEach(function(Ut){ke=ke&&!!document.queryCommandSupported(Ut)}),ke}}]),M}(s()),Ii=Ri},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,h,w){var A=c.apply(this,arguments);return l.addEventListener(u,A,w),{destroy:function(){l.removeEventListener(u,A,w)}}}function p(l,f,u,h,w){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(A){return s(A,f,u,h,w)}))}function c(l,f,u,h){return function(w){w.delegateTarget=a(w.target,f),w.delegateTarget&&h.call(l,w)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,h,w){if(!u&&!h&&!w)throw new Error("Missing required arguments");if(!a.string(h))throw new TypeError("Second argument must be a String");if(!a.fn(w))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,h,w);if(a.nodeList(u))return l(u,h,w);if(a.string(u))return f(u,h,w);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,h,w){return u.addEventListener(h,w),{destroy:function(){u.removeEventListener(h,w)}}}function l(u,h,w){return Array.prototype.forEach.call(u,function(A){A.addEventListener(h,w)}),{destroy:function(){Array.prototype.forEach.call(u,function(A){A.removeEventListener(h,w)})}}}function f(u,h,w){return s(document.body,u,h,w)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var ts=/["'&<>]/;ei.exports=rs;function rs(e){var t=""+e,r=ts.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||s(u,h)})})}function s(u,h){try{p(o[u](h))}catch(w){f(i[0][3],w)}}function p(u){u.value instanceof nt?Promise.resolve(u.value.v).then(c,l):f(i[0][2],u)}function c(u){s("next",u)}function l(u){s("throw",u)}function f(u,h){u(h),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof de=="function"?de(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function k(e){return typeof e=="function"}function ft(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ft(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=de(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(A){t={error:A}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(k(l))try{l()}catch(A){i=A instanceof zt?A.errors:[A]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=de(f),h=u.next();!h.done;h=u.next()){var w=h.value;try{fo(w)}catch(A){i=i!=null?i:[],A instanceof zt?i=q(q([],N(i)),N(A.errors)):i.push(A)}}}catch(A){o={error:A}}finally{try{h&&!h.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)fo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Fe.EMPTY;function qt(e){return e instanceof Fe||e&&"closed"in e&&k(e.remove)&&k(e.add)&&k(e.unsubscribe)}function fo(e){k(e)?e():e.unsubscribe()}var $e={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var ut={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Fe(function(){o.currentObservers=null,qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Eo(r,o)},t}(F);var Eo=function(e){re(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){re(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var Lt={now:function(){return(Lt.delegate||Date).now()},delegate:void 0};var _t=function(e){re(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Lt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(vt);var So=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(gt);var Hr=new So(To);var Oo=function(e){re(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=bt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(bt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(vt);var Mo=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(gt);var me=new Mo(Oo);var O=new F(function(e){return e.complete()});function Yt(e){return e&&k(e.schedule)}function kr(e){return e[e.length-1]}function Xe(e){return k(kr(e))?e.pop():void 0}function He(e){return Yt(kr(e))?e.pop():void 0}function Bt(e,t){return typeof kr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return k(e==null?void 0:e.then)}function Jt(e){return k(e[ht])}function Xt(e){return Symbol.asyncIterator&&k(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Gi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Gi();function tr(e){return k(e==null?void 0:e[er])}function rr(e){return lo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return k(e==null?void 0:e.getReader)}function W(e){if(e instanceof F)return e;if(e!=null){if(Jt(e))return Ji(e);if(xt(e))return Xi(e);if(Gt(e))return Zi(e);if(Xt(e))return Lo(e);if(tr(e))return ea(e);if(or(e))return ta(e)}throw Zt(e)}function Ji(e){return new F(function(t){var r=e[ht]();if(k(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Xi(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?Be(t):zo(function(){return new ir}))}}function Fr(e){return e<=0?function(){return O}:y(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,h=0,w=!1,A=!1,te=function(){f==null||f.unsubscribe(),f=void 0},ie=function(){te(),l=u=void 0,w=A=!1},J=function(){var H=l;ie(),H==null||H.unsubscribe()};return y(function(H,mt){h++,!A&&!w&&te();var ze=u=u!=null?u:r();mt.add(function(){h--,h===0&&!A&&!w&&(f=Wr(J,p))}),ze.subscribe(mt),!l&&h>0&&(l=new at({next:function(Ie){return ze.next(Ie)},error:function(Ie){A=!0,te(),f=Wr(ie,n,Ie),ze.error(Ie)},complete:function(){w=!0,te(),f=Wr(ie,a),ze.complete()}}),W(H).subscribe(l))})(c)}}function Wr(e,t){for(var r=[],o=2;oe.next(document)),e}function $(e,t=document){return Array.from(t.querySelectorAll(e))}function P(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Re(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var xa=S(d(document.body,"focusin"),d(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Re()||document.body),G(1));function et(e){return xa.pipe(m(t=>e.contains(t)),K())}function kt(e,t){return C(()=>S(d(e,"mouseenter").pipe(m(()=>!0)),d(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Me(+!r*t)):le,Q(e.matches(":hover"))))}function Bo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Bo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Bo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function wt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),S(d(t,"load"),d(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),L(()=>document.head.removeChild(t)),Te(1))))}var Go=new g,ya=C(()=>typeof ResizeObserver=="undefined"?wt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Go.next(t)))),v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),G(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return ya.pipe(E(r=>r.observe(t)),v(r=>Go.pipe(b(o=>o.target===t),L(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function Tt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Jo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ue(e){return{x:e.offsetLeft,y:e.offsetTop}}function Xo(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Zo(e){return S(d(window,"load"),d(window,"resize")).pipe(Le(0,me),m(()=>Ue(e)),Q(Ue(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function De(e){return S(d(e,"scroll"),d(window,"scroll"),d(window,"resize")).pipe(Le(0,me),m(()=>pr(e)),Q(pr(e)))}var en=new g,Ea=C(()=>I(new IntersectionObserver(e=>{for(let t of e)en.next(t)},{threshold:0}))).pipe(v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),G(1));function tt(e){return Ea.pipe(E(t=>t.observe(e)),v(t=>en.pipe(b(({target:r})=>r===e),L(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function tn(e,t=16){return De(e).pipe(m(({y:r})=>{let o=ce(e),n=Tt(e);return r>=n.height-o.height-t}),K())}var lr={drawer:P("[data-md-toggle=drawer]"),search:P("[data-md-toggle=search]")};function rn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function Ve(e){let t=lr[e];return d(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function wa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ta(){return S(d(window,"compositionstart").pipe(m(()=>!0)),d(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function on(){let e=d(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:rn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Re();if(typeof o!="undefined")return!wa(o,r)}return!0}),pe());return Ta().pipe(v(t=>t?O:e))}function xe(){return new URL(location.href)}function pt(e,t=!1){if(B("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function nn(){return new g}function an(){return location.hash.slice(1)}function sn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Sa(e){return S(d(window,"hashchange"),e).pipe(m(an),Q(an()),b(t=>t.length>0),G(1))}function cn(e){return Sa(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function $t(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function pn(){let e=matchMedia("print");return S(d(window,"beforeprint").pipe(m(()=>!0)),d(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():O))}function zr(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function Ne(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),G(1))}function ln(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),G(1))}function mn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),G(1))}function fn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function un(){return S(d(window,"scroll",{passive:!0}),d(window,"resize",{passive:!0})).pipe(m(fn),Q(fn()))}function dn(){return{width:innerWidth,height:innerHeight}}function hn(){return d(window,"resize",{passive:!0}).pipe(m(dn),Q(dn()))}function bn(){return z([un(),hn()]).pipe(m(([e,t])=>({offset:e,size:t})),G(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(Z("size")),n=z([o,r]).pipe(m(()=>Ue(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function Oa(e){return d(e,"message",t=>t.data)}function Ma(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function vn(e,t=new Worker(e)){let r=Oa(t),o=Ma(t),n=new g;n.subscribe(o);let i=o.pipe(X(),ne(!0));return n.pipe(X(),Pe(r.pipe(U(i))),pe())}var La=P("#__config"),St=JSON.parse(La.textContent);St.base=`${new URL(St.base,xe())}`;function ye(){return St}function B(e){return St.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?St.translations[e].replace("#",t.toString()):St.translations[e]}function Se(e,t=document){return P(`[data-md-component=${e}]`,t)}function ae(e,t=document){return $(`[data-md-component=${e}]`,t)}function _a(e){let t=P(".md-typeset > :first-child",e);return d(t,"click",{once:!0}).pipe(m(()=>P(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function gn(e){if(!B("announce.dismiss")||!e.childElementCount)return O;if(!e.hidden){let t=P(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),_a(e).pipe(E(r=>t.next(r)),L(()=>t.complete()),m(r=>R({ref:e},r)))})}function Aa(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function xn(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Aa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))}function Pt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function yn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function En(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function wn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,c)," "],[]).slice(0,-1),i=ye(),a=new URL(e.location,i.base);B("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=ye();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)}),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Tn(e){let t=e[0].score,r=[...e],o=ye(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreqr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function Sn(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Qr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function On(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ca(e){var o;let t=ye(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Mn(e,t){var o;let r=ye();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ca)))}var Ha=0;function ka(e){let t=z([et(e),kt(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Jo(e)).pipe(oe(De),ct(1),m(()=>Xo(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function $a(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ha++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(X(),ne(!1)).subscribe(a);let s=a.pipe(Ht(c=>Me(+!c*250,Hr)),K(),v(c=>c?r:O),E(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>kt(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),ee(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),h=u.width/2;if(l.role==="tooltip")return{x:h,y:8+u.height};if(u.y>=f.height/2){let{height:w}=ce(l);return{x:h,y:-16-w}}else return{x:h,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),ee(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(P(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),be(me),ee(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ka(e).pipe(E(c=>i.next(c)),L(()=>i.complete()),m(c=>R({ref:e},c)))})}function lt(e,{viewport$:t},r=document.body){return $a(e,{content$:new F(o=>{let n=e.title,i=yn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Pa(e,t){let r=C(()=>z([Zo(e),De(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function Ln(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(U(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),S(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Le(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),d(n,"click").pipe(U(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),d(n,"mousedown").pipe(U(a),ee(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Re())==null||c.blur()}}),r.pipe(U(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Pa(e,t).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function Ra(e){return e.tagName==="CODE"?$(".c, .c1, .cm",e):[e]}function Ia(e){let t=[];for(let r of Ra(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function _n(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Ia(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,En(p,i)),s.replaceWith(a.get(p)))}return a.size===0?O:C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=[];for(let[l,f]of a)c.push([P(".md-typeset",f),P(`:scope > li:nth-child(${l})`,e)]);return o.pipe(U(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?_n(f,u):_n(u,f)}),S(...[...a].map(([,l])=>Ln(l,t,{target$:r}))).pipe(L(()=>s.complete()),pe())})}function An(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return An(t)}}function Cn(e,t){return C(()=>{let r=An(e);return typeof r!="undefined"?fr(r,e,t):O})}var Hn=Vt(Yr());var Fa=0;function kn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return kn(t)}}function ja(e){return ge(e).pipe(m(({width:t})=>({scrollable:Tt(e).width>t})),Z("scrollable"))}function $n(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(Fr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Hn.default.isSupported()&&(e.closest(".copy")||B("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Fa++}`;let l=wn(c.id);c.insertBefore(l,e),B("content.tooltips")&&a.push(lt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=kn(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||B("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(U(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:O)))}}return $(":scope > span[id]",e).length&&e.classList.add("md-code__content"),ja(e).pipe(E(c=>n.next(c)),L(()=>n.complete()),m(c=>R({ref:e},c)),Pe(...a))});return B("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function Wa(e,{target$:t,print$:r}){let o=!0;return S(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),E(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Pn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),Wa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}var Rn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Br,Da=0;function Va(){return typeof mermaid=="undefined"||mermaid instanceof Element?wt("https://unpkg.com/mermaid@10/dist/mermaid.min.js"):I(void 0)}function In(e){return e.classList.remove("mermaid"),Br||(Br=Va().pipe(E(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Rn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),G(1))),Br.subscribe(()=>ao(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Da++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Br.pipe(m(()=>({ref:e})))}var Fn=x("table");function jn(e){return e.replaceWith(Fn),Fn.replaceWith(On(e)),I({ref:e})}function Na(e){let t=e.find(r=>r.checked)||e[0];return S(...e.map(r=>d(r,"change").pipe(m(()=>P(`label[for="${r.id}"]`))))).pipe(Q(P(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Wn(e,{viewport$:t,target$:r}){let o=P(".tabbed-labels",e),n=$(":scope > input",e),i=Qr("prev");e.append(i);let a=Qr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(X(),ne(!0));z([s,ge(e),tt(e)]).pipe(U(p),Le(1,me)).subscribe({next([{active:c},l]){let f=Ue(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let h=pr(o);(f.xh.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([De(o),ge(o)]).pipe(U(p)).subscribe(([c,l])=>{let f=Tt(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),S(d(i,"click").pipe(m(()=>-1)),d(a,"click").pipe(m(()=>1))).pipe(U(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(U(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=P(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),d(l.firstElementChild,"click").pipe(U(p),b(f=>!(f.metaKey||f.ctrlKey)),E(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return B("content.tabs.link")&&s.pipe(Ce(1),ee(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let w of $("[data-tabs]"))for(let A of $(":scope > input",w)){let te=P(`label[for="${A.id}"]`);if(te!==c&&te.innerText.trim()===f){te.setAttribute("data-md-switching",""),A.click();break}}window.scrollTo({top:e.offsetTop-u});let h=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...h])])}}),s.pipe(U(p)).subscribe(()=>{for(let c of $("audio, video",e))c.pause()}),Na(n).pipe(E(c=>s.next(c)),L(()=>s.complete()),m(c=>R({ref:e},c)))}).pipe(Qe(se))}function Un(e,{viewport$:t,target$:r,print$:o}){return S(...$(".annotate:not(.highlight)",e).map(n=>Cn(n,{target$:r,print$:o})),...$("pre:not(.mermaid) > code",e).map(n=>$n(n,{target$:r,print$:o})),...$("pre.mermaid",e).map(n=>In(n)),...$("table:not([class])",e).map(n=>jn(n)),...$("details",e).map(n=>Pn(n,{target$:r,print$:o})),...$("[data-tabs]",e).map(n=>Wn(n,{viewport$:t,target$:r})),...$("[title]",e).filter(()=>B("content.tooltips")).map(n=>lt(n,{viewport$:t})))}function za(e,{alert$:t}){return t.pipe(v(r=>S(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function Dn(e,t){let r=P(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),za(e,t).pipe(E(n=>o.next(n)),L(()=>o.complete()),m(n=>R({ref:e},n)))})}var qa=0;function Qa(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?De(o):I({x:0,y:0}),i=S(et(t),kt(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ue(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Vn(e){let t=e.title;if(!t.length)return O;let r=`__tooltip_${qa++}`,o=Pt(r,"inline"),n=P(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),S(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Le(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Qa(o,e).pipe(E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))}).pipe(Qe(se))}function Ka({viewport$:e}){if(!B("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Ye(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=Ve("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Nn(e,t){return C(()=>z([ge(e),Ka(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),G(1))}function zn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(X(),ne(!0));o.pipe(Z("active"),We(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue($("[title]",e)).pipe(b(()=>B("content.tooltips")),oe(a=>Vn(a)));return r.subscribe(o),t.pipe(U(n),m(a=>R({ref:e},a)),Pe(i.pipe(U(n))))})}function Ya(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),Z("active"))}function qn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?O:Ya(o,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))})}function Qn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),Z("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function Ba(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(oe(o=>d(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),G(1))}function Kn(e){let t=$("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=$t("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),ee(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(be(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),Ba(t).pipe(U(n.pipe(Ce(1))),st(),E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))})}function Yn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(E(o=>r.next({value:o})),L(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Gr=Vt(Yr());function Ga(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Bn({alert$:e}){Gr.default.isSupported()&&new F(t=>{new Gr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Ga(P(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(E(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function Gn(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function Ja(e,t){let r=new Map;for(let o of $("url",e)){let n=P("loc",o),i=[Gn(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of $("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(Gn(new URL(s),t))}}return r}function ur(e){return mn(new URL("sitemap.xml",e)).pipe(m(t=>Ja(t,new URL(e))),ve(()=>I(new Map)))}function Xa(e,t){if(!(e.target instanceof Element))return O;let r=e.target.closest("a");if(r===null)return O;if(r.target||e.metaKey||e.ctrlKey)return O;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):O}function Jn(e){let t=new Map;for(let r of $(":scope > *",e.head))t.set(r.outerHTML,r);return t}function Xn(e){for(let t of $("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function Za(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...B("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=Jn(document);for(let[o,n]of Jn(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return je($("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),O}),X(),ne(document))}function Zn({location$:e,viewport$:t,progress$:r}){let o=ye();if(location.protocol==="file:")return O;let n=ur(o.base);I(document).subscribe(Xn);let i=d(document.body,"click").pipe(We(n),v(([p,c])=>Xa(p,c)),pe()),a=d(window,"popstate").pipe(m(xe),pe());i.pipe(ee(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),S(i,a).subscribe(e);let s=e.pipe(Z("pathname"),v(p=>ln(p,{progress$:r}).pipe(ve(()=>(pt(p,!0),O)))),v(Xn),v(Za),pe());return S(s.pipe(ee(e,(p,c)=>c)),s.pipe(v(()=>e),Z("pathname"),v(()=>e),Z("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),E(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",sn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),d(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(Z("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ri=Vt(ti());function oi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ri.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function It(e){return e.type===1}function dr(e){return e.type===3}function ni(e,t){let r=vn(e);return S(I(location.protocol!=="file:"),Ve("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:B("search.suggest")}}})),r}function ii({document$:e}){let t=ye(),r=Ne(new URL("../versions.json",t.base)).pipe(ve(()=>O)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>d(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),ee(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?O:(i.preventDefault(),I(p))}}return O}),v(i=>ur(new URL(i)).pipe(m(a=>{let p=xe().href.replace(t.base,i);return a.has(p.split("#")[0])?new URL(p):new URL(i)})))))).subscribe(n=>pt(n,!0)),z([r,o]).subscribe(([n,i])=>{P(".md-header__topic").appendChild(Mn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ns(e,{worker$:t}){let{searchParams:r}=xe();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),Ve("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=xe();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=S(t.pipe(Ae(It)),d(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),G(1))}function ai(e,{worker$:t}){let r=new g,o=r.pipe(X(),ne(!0));z([t.pipe(Ae(It)),r],(i,a)=>a).pipe(Z("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(Z("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),d(e.form,"reset").pipe(U(o)).subscribe(()=>e.focus());let n=P("header [for=__search]");return d(n,"click").subscribe(()=>e.focus()),ns(e,{worker$:t}).pipe(E(i=>r.next(i)),L(()=>r.complete()),m(i=>R({ref:e},i)),G(1))}function si(e,{worker$:t,query$:r}){let o=new g,n=tn(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=P(":scope > :first-child",e),s=P(":scope > :last-child",e);Ve("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(ee(r),Ur(t.pipe(Ae(It)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(E(()=>s.innerHTML=""),v(({items:l})=>S(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Ye(4),Vr(n),v(([f])=>f)))),m(Tn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(oe(l=>{let f=fe("details",l);return typeof f=="undefined"?O:d(f,"toggle").pipe(U(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(E(l=>o.next(l)),L(()=>o.complete()),m(l=>R({ref:e},l)))}function is(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=xe();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function ci(e,t){let r=new g,o=r.pipe(X(),ne(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),d(e,"click").pipe(U(o)).subscribe(n=>n.preventDefault()),is(e,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))}function pi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=S(d(n,"keydown"),d(n,"focus")).pipe(be(se),m(()=>n.value),K());return o.pipe(We(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(E(s=>o.next(s)),L(()=>o.complete()),m(()=>({ref:e})))}function li(e,{index$:t,keyboard$:r}){let o=ye();try{let n=ni(o.search,t),i=Se("search-query",e),a=Se("search-result",e);d(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Re();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of $(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,h])=>h-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...$(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Re()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=ai(i,{worker$:n});return S(s,si(a,{worker$:n,query$:s})).pipe(Pe(...ae("search-share",e).map(p=>ci(p,{query$:s})),...ae("search-suggest",e).map(p=>pi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ke}}function mi(e,{index$:t,location$:r}){return z([t,r.pipe(Q(xe()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>oi(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function as(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Jr(e,o){var n=o,{header$:t}=n,r=io(n,["header$"]);let i=P(".md-sidebar__scrollwrap",e),{y:a}=Ue(i);return C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=s.pipe(Le(0,me));return c.pipe(ee(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of $(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2})}}}),ue($("label[tabindex]",e)).pipe(oe(l=>d(l,"click").pipe(be(se),m(()=>l),U(p)))).subscribe(l=>{let f=P(`[id="${l.htmlFor}"]`);P(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),as(e,r).pipe(E(l=>s.next(l)),L(()=>s.complete()),m(l=>R({ref:e},l)))})}function fi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Ct(Ne(`${r}/releases/latest`).pipe(ve(()=>O),m(o=>({version:o.tag_name})),Be({})),Ne(r).pipe(ve(()=>O),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Be({}))).pipe(m(([o,n])=>R(R({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return Ne(r).pipe(m(o=>({repositories:o.public_repos})),Be({}))}}function ui(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Ne(r).pipe(ve(()=>O),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Be({}))}function di(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return fi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ui(r,o)}return O}var ss;function cs(e){return ss||(ss=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return O}return di(e.href).pipe(E(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>O),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),G(1)))}function hi(e){let t=P(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(Sn(o)),t.classList.add("md-source__repository--active")}),cs(e).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ps(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),Z("hidden"))}function bi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(B("navigation.tabs.sticky")?I({hidden:!1}):ps(e,t)).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ls(e,{viewport$:t,header$:r}){let o=new Map,n=$(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(Z("height"),m(({height:s})=>{let p=Se("main"),c=P(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(Z("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let h=f.offsetParent;for(;h;h=h.offsetParent)u+=h.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),We(i),v(([p,c])=>t.pipe(jr(([l,f],{offset:{y:u},size:h})=>{let w=u+h.height>=Math.floor(s.height);for(;f.length;){let[,A]=f[0];if(A-c=u&&!w)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Ye(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(X(),ne(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),B("toc.follow")){let s=S(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),We(o.pipe(be(se))),ee(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2,behavior:c})}}})}return B("navigation.tracking")&&t.pipe(U(a),Z("offset"),_e(250),Ce(1),U(n.pipe(Ce(1))),st({delay:250}),ee(i)).subscribe(([,{prev:s}])=>{let p=xe(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),ls(e,{viewport$:t,header$:r}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function ms(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Ye(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),U(o.pipe(Ce(1))),ne(!0),st({delay:250}),m(a=>({hidden:a})))}function gi(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(U(a),Z("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),d(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),ms(e,{viewport$:t,main$:o,target$:n}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))}function xi({document$:e,viewport$:t}){e.pipe(v(()=>$(".md-ellipsis")),oe(r=>tt(r).pipe(U(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,B("content.tooltips")?lt(n,{viewport$:t}).pipe(U(e.pipe(Ce(1))),L(()=>n.removeAttribute("title"))):O})).subscribe(),B("content.tooltips")&&e.pipe(v(()=>$(".md-status")),oe(r=>lt(r,{viewport$:t}))).subscribe()}function yi({document$:e,tablet$:t}){e.pipe(v(()=>$(".md-toggle--indeterminate")),E(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>d(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),ee(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function fs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ei({document$:e}){e.pipe(v(()=>$("[data-md-scrollfix]")),E(t=>t.removeAttribute("data-md-scrollfix")),b(fs),oe(t=>d(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function wi({viewport$:e,tablet$:t}){z([Ve("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),ee(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function us(){return location.protocol==="file:"?wt(`${new URL("search/search_index.js",Xr.base)}`).pipe(m(()=>__index),G(1)):Ne(new URL("search/search_index.json",Xr.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Yo(),jt=nn(),Ot=cn(jt),Zr=on(),Oe=bn(),hr=$t("(min-width: 960px)"),Si=$t("(min-width: 1220px)"),Oi=pn(),Xr=ye(),Mi=document.forms.namedItem("search")?us():Ke,eo=new g;Bn({alert$:eo});var to=new g;B("navigation.instant")&&Zn({location$:jt,viewport$:Oe,progress$:to}).subscribe(ot);var Ti;((Ti=Xr.version)==null?void 0:Ti.provider)==="mike"&&ii({document$:ot});S(jt,Ot).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});Zr.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&&pt(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&&pt(r);break;case"Enter":let o=Re();o instanceof HTMLLabelElement&&o.click()}});xi({viewport$:Oe,document$:ot});yi({document$:ot,tablet$:hr});Ei({document$:ot});wi({viewport$:Oe,tablet$:hr});var rt=Nn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Qn(e,{viewport$:Oe,header$:rt})),G(1)),ds=S(...ae("consent").map(e=>xn(e,{target$:Ot})),...ae("dialog").map(e=>Dn(e,{alert$:eo})),...ae("header").map(e=>zn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("palette").map(e=>Kn(e)),...ae("progress").map(e=>Yn(e,{progress$:to})),...ae("search").map(e=>li(e,{index$:Mi,keyboard$:Zr})),...ae("source").map(e=>hi(e))),hs=C(()=>S(...ae("announce").map(e=>gn(e)),...ae("content").map(e=>Un(e,{viewport$:Oe,target$:Ot,print$:Oi})),...ae("content").map(e=>B("search.highlight")?mi(e,{index$:Mi,location$:jt}):O),...ae("header-title").map(e=>qn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Si,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>bi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>vi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})),...ae("top").map(e=>gi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})))),Li=ot.pipe(v(()=>hs),Pe(ds),G(1));Li.subscribe();window.document$=ot;window.location$=jt;window.target$=Ot;window.keyboard$=Zr;window.viewport$=Oe;window.tablet$=hr;window.screen$=Si;window.print$=Oi;window.alert$=eo;window.progress$=to;window.component$=Li;})(); +//# sourceMappingURL=bundle.fe8b6f2b.min.js.map + diff --git a/assets/javascripts/bundle.fe8b6f2b.min.js.map b/assets/javascripts/bundle.fe8b6f2b.min.js.map new file mode 100644 index 000000000..82635852a --- /dev/null +++ b/assets/javascripts/bundle.fe8b6f2b.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/clipboard/dist/clipboard.js", "node_modules/escape-html/index.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/rxjs/node_modules/tslib/tslib.es6.js", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an