Skip to content

Commit 064cad0

Browse files
authored
Merge branch 'master' into material-upgrade
2 parents 6a42fd3 + 71ae11d commit 064cad0

File tree

11 files changed

+305
-15
lines changed

11 files changed

+305
-15
lines changed

.github/workflows/publication_maven.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
runs-on: ubuntu-latest
2121
steps:
2222
- uses: actions/checkout@v2
23-
- uses: actions/cache@v1
23+
- uses: actions/cache@v3
2424
with:
2525
path: ~/.gradle/caches
2626
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}

.github/workflows/pull_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-latest
1010
steps:
1111
- uses: actions/checkout@v2
12-
- uses: actions/cache@v1
12+
- uses: actions/cache@v3
1313
with:
1414
path: ~/.gradle/caches
1515
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}

.github/workflows/snapshot_check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- uses: actions/checkout@v2
18-
- uses: actions/cache@v1
18+
- uses: actions/cache@v3
1919
with:
2020
path: ~/.gradle/caches
2121
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
id("convention.dependency-updates")
77
id("io.gitlab.arturbosch.detekt") version "1.21.0"
88
id("convention.air")
9+
alias(libs.plugins.compose.compiler) apply false
910
}
1011

1112
buildscript {

compose-support/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ publish {
1515

1616
dependencies {
1717
api(libs.kakaoCompose)
18+
api(libs.kakaoTest)
19+
api(libs.kakaoUi)
20+
api(libs.kakaoSemantics)
1821
api(libs.composeUiTest)
1922

2023
implementation(projects.kaspresso)
125 KB
Loading
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
## What is Pixel-by-Pixel Comparison?
2+
3+
This type of test involves the following process:
4+
1) You write an autotest that brings your application to a certain state and takes a screenshot
5+
2) The first run of the autotest is performed in screenshot recording mode. The screenshots are saved in your infrastructure.
6+
3) Each subsequent run of the autotest is performed in a comparison mode. The autotest compares the screenshots from the previous step with the new ones.
7+
8+
If the differences between the screenshots do not exceed the threshold values, the test is considered to be successful.
9+
If the differences exceed the threshold values, the test fails. In case of a comparison failure, Kaspresso generates an image highlighting
10+
the differences between the screenshots.
11+
12+
In the example below, the first screenshot is the reference, the second is the new one, and the third is the comparison result.
13+
![pixel-by-pixel-comparison](Images/pixel_by_pixel_comparison/flow.png)
14+
15+
## How to Use Pixel-by-Pixel Comparison
16+
### Code
17+
You can see an example of code in [VisualTestSample.kt](https://github.com/KasperskyLab/Kaspresso/blob/master/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/visual/VisualTestSample.kt)
18+
19+
Next, we will break down the key points to pay attention to.
20+
21+
A new autotest should be a subclass of VisualTestCase:
22+
```kotlin
23+
class VisualTest : VisualTestCase() {
24+
// ...
25+
}
26+
```
27+
28+
In the test method, instead of the usual `before{}.after{}.run{}` call, you need to use the `runScreenshotTest`:
29+
```kotlin
30+
@Test
31+
fun test() = runScreenshotTest {
32+
step("Open Simple Screen") {
33+
MainScreen {
34+
simpleButton {
35+
isVisible()
36+
click()
37+
assertScreenshot("some_tag")
38+
}
39+
}
40+
}
41+
}
42+
```
43+
If you need to perform any actions before or after the test, you can pass the `before`
44+
and `after` blocks to `runScreenshotTest`:
45+
```kotlin
46+
@Test
47+
fun test() = runScreenshotTest(
48+
before = {},
49+
after = {},
50+
) {
51+
// Test code
52+
}
53+
```
54+
55+
Note the call to `assertScreenshot()`. Ше will save a new screenshot in the recording mode and compare it with the reference in the comparison mode.
56+
57+
### Running
58+
Pixel-by-pixel comparison tests can work in the two modes - `Record` and `Compare`.
59+
When you need to record the reference screenshots, use the `Record` mode.
60+
When the test code is stabilized, run it in `Compare` mode. The line which recorded
61+
a screenshot in the `Record` mode, now does the comparison with the reference.
62+
63+
There are two ways to choose the test mode:
64+
1) Manually in the code, by passing a parameter to the VisualTestCase constructor:
65+
```kotlin
66+
class VisualTestSample : VisualTestCase(
67+
kaspressoBuilder = Kaspresso.Builder.simple {
68+
visualTestParams = VisualTestParams(testType = VisualTestType.Record)
69+
}
70+
)
71+
```
72+
2) Set the Gradle property:
73+
```groovy
74+
kaspresso.visualTestType="Record"
75+
```
76+
77+
**Important** - before running the test, you need to start the adb server. You can find out how to do this [here](Executing_adb_commands.en.md)
78+
79+
After running the test in the recording mode, the screenshots will be saved to the device's memory.
80+
By default, it will be saved in the `/sdcard/Documents/original_screenshots/` folder.
81+
Save those screenshots somewhere in your infrastructure. The easiest way is to put them in
82+
a repository along with the application sources or the same place where you store
83+
the adb-server jar file.
84+
85+
To make subsequent test runs compare screenshots, you need to switch the test mode
86+
to `Compare`. If significant differences are found between the screenshots,
87+
Kaspresso will create a `screenshot_diffs` folder containing auxiliary screenshots
88+
with differences between the references and new screenshots, and the test will fail.
89+
90+
## How Comparison Works
91+
Each screenshot is represented as a two-dimensional array. Each value in the array is
92+
the color of a pixel. The test goes through two arrays (reference and new),
93+
compares individual pixels and calculates the ratio of the number of the
94+
pixels that do differ to the total number of pixels.
95+
96+
This type of test should be run on the same device configuration (preferably on the same AVD),
97+
but even in this case, the same test can generate the screenshots that differ from each
98+
other by several pixels. This is mainly due to the peculiarities of rendering on
99+
different hardware.
100+
101+
To solve this problem, you can set a threshold value for comparison. If the difference
102+
between the reference and the new screenshot is less than the specified value, the
103+
test will be considered successful. The default threshold value is 0.3%.
104+
105+
The threshold value is set by the `tolerance` field in the `VisualTestParams` class.
106+
107+
To determine if an individual pixel differs, a comparison of each of the RGB channels of
108+
the reference and the original is performed. If the difference between at least one of the
109+
channels is greater than the value in the `colorTolerance` field of the `VisualTestParams`
110+
class, the pixel is considered different. Note that the value in the channel ranges from
111+
0 to 255.
112+
113+
## Test Configuration
114+
For more fine-grained configuration of screenshot comparison, the `VisualTestParams` class
115+
is used. You need to set it up in the Kaspresso builder:
116+
```kotlin
117+
class VisualTestSample : VisualTestCase(kaspressoBuilder = Kaspresso.Builder.simple {
118+
visualTestParams = VisualTestParams(
119+
testType = VisualTestType.Compare,
120+
hostScreenshotsDir = "original_screenshots",
121+
colorTolerance = 1,
122+
tolerance = 0.3f
123+
)
124+
}) {
125+
// ...
126+
}
127+
```
128+
You can read more about what each parameter is responsible for in the Javadoc.
129+
130+
## Allure Support
131+
If you use Allure to generate reports and want to configure pixel-by-pixel comparison tests,
132+
you need to make the test class a subclass of `AllureVisualTestCase`:
133+
134+
```kotlin
135+
class VisualTest : AllureVisualTestCase() {
136+
// ...
137+
}
138+
```
139+
A step with a failed comparison will be marked in the report. To make the test fail on
140+
the first failed comparison, you need to pass the `failEarly` parameter with a value
141+
of `true` to the `AllureVisualTestCase` constructor:
142+
```kotlin
143+
class VisualTest : AllureVisualTestCase(failEarly = true) {
144+
// ...
145+
}
146+
```
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
## Что такое попиксельное сравнение?
2+
3+
Этот вид тестов предполагает следующий процесс:
4+
1) Вы пишете автотест, который приводит ваше приложение к определенному состоянию и делает скриншот
5+
2) Первый запуск автотеста вы выполняете в режиме записи скриншотов. Скриншоты вы сохраняете в своей инфраструктуре.
6+
3) Каждый последующий запуск автотеста вы выполняете в режиме сравнения. Автотест сравнивает скриншоты с
7+
с предыдущего шага с новыми
8+
9+
Если различия между скриншотами не превышают пороговые значения, тест считается успешным. Если отличия превышают
10+
предельные значения, то тест не проходит. В случае обнаружения отличий Kaspresso генерирует картинку, подсвечивающую различия между скриншотами.
11+
12+
В примере ниже первый скриншот - эталонный, второй - новый, а третий - результат сравнения.
13+
![pixel-by-pixel-comparison](Images/pixel_by_pixel_comparison/flow.png)
14+
15+
## Как использовать по-пиксельное сравнение
16+
### Код
17+
Пример кода пожно посмотреть в [VisualTestSample.kt](https://github.com/KasperskyLab/Kaspresso/blob/master/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/visual/VisualTestSample.kt)
18+
19+
Ниже разберем ключевые моменты, на которые стоит обратить внимание.
20+
21+
Новый автотест должен быть наследником VisualTestCase:
22+
```kotlin
23+
class VisualTest : VisualTestCase() {
24+
// ...
25+
}
26+
```
27+
28+
Для описания теста вместо вызова before{}.after{}.run{} необходимо воспользоваться методом `runScreenshotTest`:
29+
```kotlin
30+
@Test
31+
fun test() = runScreenshotTest {
32+
step("Open Simple Screen") {
33+
MainScreen {
34+
simpleButton {
35+
isVisible()
36+
click()
37+
assertScreenshot("some_tag")
38+
}
39+
}
40+
}
41+
}
42+
```
43+
Если вам необходимо выполнить какие-либо действия до или после выполнения теста - передайте блоки before и
44+
after в `runScreenshotTest`:
45+
```kotlin
46+
@Test
47+
fun test() = runScreenshotTest(
48+
before = {},
49+
after = {},
50+
) {
51+
// Test code
52+
}
53+
```
54+
55+
Обратите внимание на вызов `assertScreenshot()`. Эта конструкция в режиме записи сохранит новый скриншот,
56+
а в режиме сравнения - сравнит его с эталонным.
57+
58+
### Запуск
59+
Тесты сравнения скриншотов могут работать в двух режимах - `Record` и `Compare`. При необходимости записи эталонных скриншотов
60+
используется режим `Record`. Когда тест готов - он запускается в режиме compare. На шаге, где в режиме `Record` происходила
61+
запись скриншота теперь выполняется сравнение с эталоном.
62+
63+
Есть 2 способа выбрать режим работы теста:
64+
1) Вручную в коде, передавая параметр в конструктор VisualTestCase:
65+
```kotlin
66+
class VisualTestSample : VisualTestCase(
67+
kaspressoBuilder = Kaspresso.Builder.simple {
68+
visualTestParams = VisualTestParams(testType = VisualTestType.Record)
69+
}
70+
)
71+
```
72+
2) Задать значение gradle property:
73+
```groovy
74+
kaspresso.visualTestType="Record"
75+
```
76+
77+
**Важно** - перед запуском теста необходимо запустить adb server. О том как это сделать написано
78+
[здесь](Executing_adb_commands.ru.md)
79+
80+
После запуска теста в режиме записи, скриншоты будут сохранены в памяти устройства. По умолчанию они будут сохранены в папке
81+
`/sdcard/Documents/original_screenshots/`. Полученные скриншоты вы должны сохранить где-то в своей инфрастуктуре.
82+
Легче всего будет поместить их в репозиторий рядом с исходниками приложения или сохранить там же, где вы храните jar файл adb-server'а.
83+
84+
Чтобы последующие запуски автотеста производили сравнение скриншотов, необходимо переключить режим проведения тестов
85+
в значение `Compare`. В случше, если между скриншотами будут найдены существенные отличия, Kaspresso сформирует папку
86+
`screenshot_diffs`, в которой будут лежать вспомогательные скриншоты с отличиями между эталонами и новыми скриншотами, а тест упадет.
87+
88+
## Как происходит сравнение
89+
Каждый скриншот представляется в виде двухмерного массива. Каждое значение в массиве - это цвет пикселя.
90+
Тест проходит по двум массивам (эталон и новый), сравнивает индивидуальные пиксели и считает отношение между
91+
количеством отличающихся пикселей и общего количества пикселей.
92+
93+
Данный вид тестов необходимо запускать на одной и той же конфигурации устройств (предпочтительно на одном и том же AVD),
94+
но даже в таком случае один и тот же тест может генерировать скриншоты, которые отличаются друг от друга несколькими пикселями.
95+
В основном, это связано с особенностями рендера на разном железе.
96+
97+
Чтобы решить эту проблему, можно установить пороговое значение для сравнения. Если разница между эталоном и новым скриншотом
98+
меньше, чем заданное значение, то тест будет считаться успешным. По умолчанию пороговое значение равно 0.3%.
99+
100+
Пороговое значение задается полем `tolerance` в классе `VisualTestParams`.
101+
102+
Для того, чтобы понять отличается ли индивидуальный пиксель, происходит сравнение каждого из RGB каналов
103+
эталона и оригинала. Если отличие между хотя бы одним из каналов больше, чем значение в поле `colorTolerance`
104+
класса `VisualTestParams`, то пиксель считается отличным. Имейте ввиду, что значение в канале лежат в диапазоне [0, 255].
105+
106+
## Конфигурация тестов
107+
Для более тонкой настройки сравнения скриншотов служит класс `VisualTestParams`.
108+
Инициализировать его необходимо в Kaspresso builder:
109+
```kotlin
110+
class VisualTestSample : VisualTestCase(kaspressoBuilder = Kaspresso.Builder.simple {
111+
visualTestParams = VisualTestParams(
112+
testType = VisualTestType.Compare,
113+
hostScreenshotsDir = "original_screenshots",
114+
colorTolerance = 1,
115+
tolerance = 0.3f
116+
)
117+
}) {
118+
// ...
119+
}
120+
```
121+
Более подробно о том, за что отвечает каждый параметр можно почитать в javadoc.
122+
123+
## Поддержка allure
124+
Если вы пользуетесь allure для генерации отчетов и хотите настроить тесты сравнения скриншотов, то необходимо чтобы
125+
тестовый класс был насоедником `AllureVisualTestCase`:
126+
127+
```kotlin
128+
class VisualTest : AllureVisualTestCase() {
129+
// ...
130+
}
131+
```
132+
Шаг с неудачным сравнение будет отмечен в отчете. Для того, чтобы тест падал при первом неудачном сравнении - необходимо
133+
передать в конструктор `AllureVisualTestCase` параметр `failEarly` со значением `true`:
134+
```kotlin
135+
class VisualTest : AllureVisualTestCase(failEarly = true) {
136+
// ...
137+
}
138+
```

gradle/libs.versions.toml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[versions]
2-
kotlin = "1.8.22"
2+
kotlin = "2.0.0"
33
detekt = "1.21.0"
44
espresso = "3.6.1"
55
kakao = "3.6.2"
6-
kakaoCompose = "0.2.3"
6+
kakaoCompose = "1.0.0"
77
kakaoExtClicks = "1.0.0"
88
allure = "2.4.0"
9-
compose = "1.5.4"
10-
composeCompiler = "1.4.8"
9+
compose = "1.7.8"
10+
composeCompiler = "1.5.15"
1111
activityCompose = "1.4.0"
1212
androidXTest = "1.6.1"
1313
testOrchestrator = "1.4.2"
@@ -38,6 +38,7 @@ detektFormatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", v
3838

3939
androidXCore = "androidx.core:core:1.12.0"
4040
appcompat = "androidx.appcompat:appcompat:1.6.1"
41+
4142
material = { module = "com.google.android.material:material", version.ref = "material" }
4243
constraint = "androidx.constraintlayout:constraintlayout:2.1.4"
4344
multidex = "androidx.multidex:multidex:2.0.1"
@@ -56,11 +57,14 @@ uiAutomator = "androidx.test.uiautomator:uiautomator:2.3.0"
5657
robolectric = "org.robolectric:robolectric:4.8.2"
5758
kakao = { module = "io.github.kakaocup:kakao", version.ref = "kakao" }
5859
kakaoCompose = { module = "io.github.kakaocup:compose", version.ref = "kakaoCompose" }
60+
kakaoTest = { module = "io.github.kakaocup:compose-test", version.ref = "kakaoCompose" }
61+
kakaoUi = { module = "io.github.kakaocup:compose-ui", version.ref = "kakaoCompose" }
62+
kakaoSemantics = { module = "io.github.kakaocup:compose-semantics", version.ref = "kakaoCompose" }
5963
kakaoExtClicks = { module = "io.github.kakaocup:kakao-ext-clicks", version.ref = "kakaoExtClicks" }
6064
junit = "junit:junit:4.13.2"
6165
junitJupiter = "org.junit.jupiter:junit-jupiter:5.9.0"
6266
assertj = "org.assertj:assertj-core:3.11.1"
63-
truth = "com.google.truth:truth:1.3.0"
67+
truth = "com.google.truth:truth:1.4.4"
6468
mockk = "io.mockk:mockk:1.13.12"
6569

6670
androidXTestCore = { module = "androidx.test:core", version.ref = "androidXTest" }
@@ -72,7 +76,6 @@ androidXTestExtJunit = "androidx.test.ext:junit:1.1.5"
7276
androidXTestExtJunitKtx = "androidx.test.ext:junit-ktx:1.1.5"
7377
androidXLifecycleRuntimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
7478

75-
composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "composeCompiler" }
7679
composeActivity = "androidx.activity:activity-compose:1.5.1"
7780
composeUiTooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
7881
composeMaterial = { module = "androidx.compose.material:material", version.ref = "compose" }
@@ -95,7 +98,8 @@ com-google-android-material-material = { group = "com.google.android.material",
9598
[bundles]
9699
espresso = ["espressoCore", "espressoWeb"]
97100
allure = ["allureKotlinModel", "allureKotlinCommons", "allureKotlinJunit4", "allureKotlinAndroid"]
98-
compose = ["composeActivity", "composeUiTooling", "composeMaterial", "composeTestManifest", "composeCompiler"]
101+
compose = ["composeActivity", "composeUiTooling", "composeMaterial", "composeTestManifest"]
99102
[plugins]
100103
com-android-library = { id = "com.android.library", version.ref = "agp" }
101104
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin-android" }
105+
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

0 commit comments

Comments
 (0)