[go: nahoru, domu]

blob: 613d4e25c01cd4f3aa0a2e993a31a50a89600045 [file] [log] [blame]
Matvei Malkov2d37cbd2019-07-04 15:15:20 +01001/*
2 * Copyright 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.ui.material
18
Jelle Fresen11fc7062020-01-13 16:53:55 +000019import android.os.SystemClock.sleep
Louis Pullen-Freiliche46bcf02020-02-12 16:06:21 +000020import androidx.compose.emptyContent
Leland Richardsonfcf76b32020-05-13 16:58:59 -070021import androidx.compose.mutableStateOf
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010022import androidx.test.filters.MediumTest
Adam Powell999a89b2020-03-11 09:08:07 -070023import androidx.ui.core.LayoutCoordinates
24import androidx.ui.core.Modifier
Matvei Malkov7aa7fd22019-08-16 16:52:59 +010025import androidx.ui.core.TestTag
George Mountd02af602020-03-06 16:41:40 -080026import androidx.ui.core.onPositioned
Matvei Malkov201726d2020-03-13 14:03:54 +000027import androidx.ui.foundation.Box
Matvei Malkov7aa7fd22019-08-16 16:52:59 +010028import androidx.ui.foundation.Clickable
Adam Powell999a89b2020-03-11 09:08:07 -070029import androidx.ui.layout.fillMaxSize
Matvei Malkov7aa7fd22019-08-16 16:52:59 +010030import androidx.ui.semantics.Semantics
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010031import androidx.ui.test.createComposeRule
Jelle Fresen11fc7062020-01-13 16:53:55 +000032import androidx.ui.test.doGesture
Matvei Malkov7aa7fd22019-08-16 16:52:59 +010033import androidx.ui.test.findByTag
Jelle Fresen11fc7062020-01-13 16:53:55 +000034import androidx.ui.test.globalBounds
Filip Pavlis6fdb2502020-04-02 14:41:32 +010035import androidx.ui.test.runOnIdleCompose
36import androidx.ui.test.runOnUiThread
Jelle Fresen11fc7062020-01-13 16:53:55 +000037import androidx.ui.test.sendClick
George Mount8baef7a2020-01-21 13:40:57 -080038import androidx.ui.unit.IntPx
39import androidx.ui.unit.IntPxSize
George Mount842c8c12020-01-08 16:03:42 -080040import androidx.ui.unit.PxPosition
George Mount842c8c12020-01-08 16:03:42 -080041import androidx.ui.unit.dp
Ryan Mentley7865a632019-08-20 20:10:29 -070042import androidx.ui.unit.height
Jelle Fresen11fc7062020-01-13 16:53:55 +000043import androidx.ui.unit.px
George Mount842c8c12020-01-08 16:03:42 -080044import androidx.ui.unit.round
Ryan Mentley7865a632019-08-20 20:10:29 -070045import androidx.ui.unit.width
Filip Pavlise63b30c2020-01-09 16:21:07 +000046import com.google.common.truth.Truth.assertThat
Matvei Malkov886ec6a2020-02-03 01:45:29 +000047import org.junit.Ignore
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010048import org.junit.Rule
49import org.junit.Test
50import org.junit.runner.RunWith
51import org.junit.runners.JUnit4
Jelle Fresen4c566312020-01-16 10:32:40 +000052import java.util.concurrent.CountDownLatch
53import java.util.concurrent.TimeUnit
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010054import kotlin.math.roundToInt
55
56@MediumTest
57@RunWith(JUnit4::class)
58class DrawerTest {
59
60 @get:Rule
Matvei Malkov7aa7fd22019-08-16 16:52:59 +010061 val composeTestRule = createComposeRule(disableTransitions = true)
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010062
63 @Test
64 fun modalDrawer_testOffset_whenOpened() {
65 var position: PxPosition? = null
66 composeTestRule.setMaterialContent {
Matvei Malkov7aa7fd22019-08-16 16:52:59 +010067 ModalDrawerLayout(DrawerState.Opened, {}, drawerContent = {
Adam Powell999a89b2020-03-11 09:08:07 -070068 Box(Modifier.fillMaxSize().onPositioned { coords: LayoutCoordinates ->
Matvei Malkov201726d2020-03-13 14:03:54 +000069 position = coords.localToGlobal(PxPosition.Origin)
70 })
Louis Pullen-Freiliche46bcf02020-02-12 16:06:21 +000071 }, bodyContent = emptyContent())
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010072 }
Filip Pavlis6fdb2502020-04-02 14:41:32 +010073 runOnIdleCompose {
Filip Pavlise63b30c2020-01-09 16:21:07 +000074 assertThat(position!!.x.value).isEqualTo(0f)
75 }
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010076 }
77
78 @Test
79 fun modalDrawer_testOffset_whenClosed() {
80 var position: PxPosition? = null
81 composeTestRule.setMaterialContent {
Matvei Malkov7aa7fd22019-08-16 16:52:59 +010082 ModalDrawerLayout(DrawerState.Closed, {}, drawerContent = {
Adam Powell999a89b2020-03-11 09:08:07 -070083 Box(Modifier.fillMaxSize().onPositioned { coords: LayoutCoordinates ->
Matvei Malkov201726d2020-03-13 14:03:54 +000084 position = coords.localToGlobal(PxPosition.Origin)
85 })
Louis Pullen-Freiliche46bcf02020-02-12 16:06:21 +000086 }, bodyContent = emptyContent())
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010087 }
88 val width = composeTestRule.displayMetrics.widthPixels
Filip Pavlis6fdb2502020-04-02 14:41:32 +010089 runOnIdleCompose {
Filip Pavlise63b30c2020-01-09 16:21:07 +000090 assertThat(position!!.x.round().value).isEqualTo(-width)
91 }
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010092 }
93
94 @Test
95 fun modalDrawer_testEndPadding_whenOpened() {
George Mount8baef7a2020-01-21 13:40:57 -080096 var size: IntPxSize? = null
Matvei Malkov2d37cbd2019-07-04 15:15:20 +010097 composeTestRule.setMaterialContent {
Matvei Malkov7aa7fd22019-08-16 16:52:59 +010098 ModalDrawerLayout(DrawerState.Opened, {}, drawerContent = {
Adam Powell999a89b2020-03-11 09:08:07 -070099 Box(Modifier.fillMaxSize().onPositioned { coords: LayoutCoordinates ->
100 size = coords.size
101 })
Louis Pullen-Freiliche46bcf02020-02-12 16:06:21 +0000102 }, bodyContent = emptyContent())
Matvei Malkov2d37cbd2019-07-04 15:15:20 +0100103 }
104
105 val width = composeTestRule.displayMetrics.widthPixels
Filip Pavlise63b30c2020-01-09 16:21:07 +0000106 composeTestRule.runOnIdleComposeWithDensity {
George Mount8baef7a2020-01-21 13:40:57 -0800107 assertThat(size!!.width.value)
Matvei Malkov2d37cbd2019-07-04 15:15:20 +0100108 .isEqualTo(width - 56.dp.toPx().round().value)
109 }
110 }
111
112 @Test
113 fun bottomDrawer_testOffset_whenOpened() {
114 var position: PxPosition? = null
115 composeTestRule.setMaterialContent {
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100116 BottomDrawerLayout(DrawerState.Opened, {}, drawerContent = {
Adam Powell999a89b2020-03-11 09:08:07 -0700117 Box(Modifier.fillMaxSize().onPositioned { coords: LayoutCoordinates ->
Matvei Malkov201726d2020-03-13 14:03:54 +0000118 position = coords.localToGlobal(PxPosition.Origin)
119 })
Louis Pullen-Freiliche46bcf02020-02-12 16:06:21 +0000120 }, bodyContent = emptyContent())
Matvei Malkov2d37cbd2019-07-04 15:15:20 +0100121 }
Filip Pavlise63b30c2020-01-09 16:21:07 +0000122
Matvei Malkov2270be12019-07-08 16:59:45 +0100123 val width = composeTestRule.displayMetrics.widthPixels
Matvei Malkov2d37cbd2019-07-04 15:15:20 +0100124 val height = composeTestRule.displayMetrics.heightPixels
Matvei Malkov2270be12019-07-08 16:59:45 +0100125 // temporary calculation of landscape screen
Matvei Malkovb5e3d8a2020-03-09 15:17:20 +0000126 val expectedHeight = if (width > height) 0 else (height / 2f).roundToInt()
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100127 runOnIdleCompose {
Filip Pavlise63b30c2020-01-09 16:21:07 +0000128 assertThat(position!!.y.round().value).isEqualTo(expectedHeight)
129 }
Matvei Malkov2d37cbd2019-07-04 15:15:20 +0100130 }
131
132 @Test
133 fun bottomDrawer_testOffset_whenClosed() {
134 var position: PxPosition? = null
135 composeTestRule.setMaterialContent {
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100136 BottomDrawerLayout(DrawerState.Closed, {}, drawerContent = {
Adam Powell999a89b2020-03-11 09:08:07 -0700137 Box(Modifier.fillMaxSize().onPositioned { coords: LayoutCoordinates ->
Matvei Malkov201726d2020-03-13 14:03:54 +0000138 position = coords.localToGlobal(PxPosition.Origin)
139 })
Louis Pullen-Freiliche46bcf02020-02-12 16:06:21 +0000140 }, bodyContent = emptyContent())
Matvei Malkov2d37cbd2019-07-04 15:15:20 +0100141 }
142 val height = composeTestRule.displayMetrics.heightPixels
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100143 runOnIdleCompose {
Filip Pavlise63b30c2020-01-09 16:21:07 +0000144 assertThat(position!!.y.round().value).isEqualTo(height)
145 }
Matvei Malkov2d37cbd2019-07-04 15:15:20 +0100146 }
147
148 @Test
Matvei Malkov886ec6a2020-02-03 01:45:29 +0000149 @Ignore("failing in postsubmit, fix in b/148751721")
Jelle Fresen4c566312020-01-16 10:32:40 +0000150 fun modalDrawer_openAndClose() {
George Mount8baef7a2020-01-21 13:40:57 -0800151 var contentWidth: IntPx? = null
Jelle Fresen4c566312020-01-16 10:32:40 +0000152 var openedLatch: CountDownLatch? = null
153 var closedLatch: CountDownLatch? = CountDownLatch(1)
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700154 val drawerState = mutableStateOf(DrawerState.Closed)
Jelle Fresen4c566312020-01-16 10:32:40 +0000155 composeTestRule.setMaterialContent {
156 TestTag("Drawer") {
157 Semantics(container = true) {
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700158 ModalDrawerLayout(drawerState.value, { drawerState.value = it },
Jelle Fresen4c566312020-01-16 10:32:40 +0000159 drawerContent = {
Matvei Malkov201726d2020-03-13 14:03:54 +0000160 Box(
Adam Powell999a89b2020-03-11 09:08:07 -0700161 Modifier.fillMaxSize().onPositioned { info: LayoutCoordinates ->
George Mountb47c3b02020-03-10 23:26:42 -0700162 val pos = info.localToGlobal(PxPosition.Origin)
163 if (pos.x == 0.px) {
164 // If fully opened, mark the openedLatch if present
165 openedLatch?.countDown()
166 } else if (-pos.x.round() == contentWidth) {
167 // If fully closed, mark the closedLatch if present
168 closedLatch?.countDown()
169 }
Jelle Fresen4c566312020-01-16 10:32:40 +0000170 }
George Mountb47c3b02020-03-10 23:26:42 -0700171 )
Jelle Fresen4c566312020-01-16 10:32:40 +0000172 },
173 bodyContent = {
Adam Powell999a89b2020-03-11 09:08:07 -0700174 Box(Modifier.fillMaxSize()
175 .onPositioned { contentWidth = it.size.width })
Jelle Fresen4c566312020-01-16 10:32:40 +0000176 })
177 }
178 }
179 }
180 // Drawer should start in closed state
181 assertThat(closedLatch!!.await(5, TimeUnit.SECONDS)).isTrue()
182
183 // When the drawer state is set to Opened
184 openedLatch = CountDownLatch(1)
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100185 runOnIdleCompose {
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700186 drawerState.value = DrawerState.Opened
Jelle Fresen4c566312020-01-16 10:32:40 +0000187 }
188 // Then the drawer should be opened
189 assertThat(openedLatch.await(5, TimeUnit.SECONDS)).isTrue()
190
191 // When the drawer state is set to Closed
192 closedLatch = CountDownLatch(1)
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100193 runOnIdleCompose {
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700194 drawerState.value = DrawerState.Closed
Jelle Fresen4c566312020-01-16 10:32:40 +0000195 }
196 // Then the drawer should be closed
197 assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue()
198 }
199
200 @Test
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100201 fun modalDrawer_bodyContent_clickable() {
202 var drawerClicks = 0
203 var bodyClicks = 0
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700204 val drawerState = mutableStateOf(DrawerState.Closed)
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100205 composeTestRule.setMaterialContent {
Aurimas Liutikas7a828d32019-10-07 17:16:05 -0700206 // emulate click on the screen
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100207 TestTag("Drawer") {
208 Semantics(container = true) {
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700209 ModalDrawerLayout(drawerState.value, { drawerState.value = it },
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100210 drawerContent = {
211 Clickable(onClick = { drawerClicks += 1 }) {
Adam Powell999a89b2020-03-11 09:08:07 -0700212 Box(Modifier.fillMaxSize(), children = emptyContent())
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100213 }
214 },
215 bodyContent = {
216 Clickable(onClick = { bodyClicks += 1 }) {
Adam Powell999a89b2020-03-11 09:08:07 -0700217 Box(Modifier.fillMaxSize(), children = emptyContent())
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100218 }
219 })
220 }
221 }
222 }
223
Jelle Fresen11fc7062020-01-13 16:53:55 +0000224 // Click in the middle of the drawer (which is the middle of the body)
225 findByTag("Drawer").doGesture { sendClick() }
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100226
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100227 runOnIdleCompose {
Filip Pavlise63b30c2020-01-09 16:21:07 +0000228 assertThat(drawerClicks).isEqualTo(0)
229 assertThat(bodyClicks).isEqualTo(1)
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100230
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700231 drawerState.value = DrawerState.Opened
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100232 }
Jelle Fresen11fc7062020-01-13 16:53:55 +0000233 sleep(100) // TODO(147586311): remove this sleep when opening the drawer triggers a wait
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100234
Jelle Fresen11fc7062020-01-13 16:53:55 +0000235 // Click on the left-center pixel of the drawer
236 findByTag("Drawer").doGesture {
Ryan Mentley7865a632019-08-20 20:10:29 -0700237 val left = 1.px
Jelle Fresen11fc7062020-01-13 16:53:55 +0000238 val centerY = globalBounds.height / 2
Ryan Mentley7865a632019-08-20 20:10:29 -0700239 sendClick(PxPosition(left, centerY))
Jelle Fresen11fc7062020-01-13 16:53:55 +0000240 }
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100241
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100242 runOnIdleCompose {
Filip Pavlise63b30c2020-01-09 16:21:07 +0000243 assertThat(drawerClicks).isEqualTo(1)
244 assertThat(bodyClicks).isEqualTo(1)
Filip Pavlis2b161e42019-11-22 17:40:08 +0000245 }
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100246 }
247
248 @Test
Matvei Malkov886ec6a2020-02-03 01:45:29 +0000249 @Ignore("failing in postsubmit, fix in b/148751721")
Jelle Fresen4c566312020-01-16 10:32:40 +0000250 fun bottomDrawer_openAndClose() {
George Mount8baef7a2020-01-21 13:40:57 -0800251 var contentHeight: IntPx? = null
252 var openedHeight: IntPx? = null
Jelle Fresen4c566312020-01-16 10:32:40 +0000253 var openedLatch: CountDownLatch? = null
254 var closedLatch: CountDownLatch? = CountDownLatch(1)
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700255 val drawerState = mutableStateOf(DrawerState.Closed)
Jelle Fresen4c566312020-01-16 10:32:40 +0000256 composeTestRule.setMaterialContent {
257 TestTag("Drawer") {
258 Semantics(container = true) {
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700259 BottomDrawerLayout(drawerState.value, { drawerState.value = it },
Jelle Fresen4c566312020-01-16 10:32:40 +0000260 drawerContent = {
Adam Powell999a89b2020-03-11 09:08:07 -0700261 Box(Modifier.fillMaxSize().onPositioned { info: LayoutCoordinates ->
Matvei Malkov201726d2020-03-13 14:03:54 +0000262 val pos = info.localToGlobal(PxPosition.Origin)
263 if (pos.y.round() == openedHeight) {
264 // If fully opened, mark the openedLatch if present
265 openedLatch?.countDown()
266 } else if (pos.y.round() == contentHeight) {
267 // If fully closed, mark the closedLatch if present
268 closedLatch?.countDown()
Jelle Fresen4c566312020-01-16 10:32:40 +0000269 }
Matvei Malkov201726d2020-03-13 14:03:54 +0000270 })
Jelle Fresen4c566312020-01-16 10:32:40 +0000271 },
272 bodyContent = {
Adam Powell999a89b2020-03-11 09:08:07 -0700273 Box(Modifier.fillMaxSize().onPositioned {
Matvei Malkov201726d2020-03-13 14:03:54 +0000274 contentHeight = it.size.height
275 openedHeight = it.size.height * BottomDrawerOpenFraction
276 })
George Mountb47c3b02020-03-10 23:26:42 -0700277 }
278 )
Jelle Fresen4c566312020-01-16 10:32:40 +0000279 }
280 }
281 }
282 // Drawer should start in closed state
283 assertThat(closedLatch!!.await(5, TimeUnit.SECONDS)).isTrue()
284
285 // When the drawer state is set to Opened
286 openedLatch = CountDownLatch(1)
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100287 runOnIdleCompose {
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700288 drawerState.value = DrawerState.Opened
Jelle Fresen4c566312020-01-16 10:32:40 +0000289 }
290 // Then the drawer should be opened
291 assertThat(openedLatch.await(5, TimeUnit.SECONDS)).isTrue()
292
293 // When the drawer state is set to Closed
294 closedLatch = CountDownLatch(1)
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100295 runOnIdleCompose {
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700296 drawerState.value = DrawerState.Closed
Jelle Fresen4c566312020-01-16 10:32:40 +0000297 }
298 // Then the drawer should be closed
299 assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue()
300 }
301
302 @Test
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100303 fun bottomDrawer_bodyContent_clickable() {
304 var drawerClicks = 0
305 var bodyClicks = 0
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700306 val drawerState = mutableStateOf(DrawerState.Closed)
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100307 composeTestRule.setMaterialContent {
Aurimas Liutikas7a828d32019-10-07 17:16:05 -0700308 // emulate click on the screen
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100309 TestTag("Drawer") {
310 Semantics(container = true) {
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700311 BottomDrawerLayout(drawerState.value, { drawerState.value = it },
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100312 drawerContent = {
313 Clickable(onClick = { drawerClicks += 1 }) {
Adam Powell999a89b2020-03-11 09:08:07 -0700314 Box(Modifier.fillMaxSize(), children = emptyContent())
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100315 }
316 },
317 bodyContent = {
318 Clickable(onClick = { bodyClicks += 1 }) {
Adam Powell999a89b2020-03-11 09:08:07 -0700319 Box(Modifier.fillMaxSize(), children = emptyContent())
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100320 }
321 })
322 }
323 }
324 }
325
Jelle Fresen11fc7062020-01-13 16:53:55 +0000326 // Click in the middle of the drawer (which is the middle of the body)
327 findByTag("Drawer").doGesture { sendClick() }
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100328
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100329 runOnIdleCompose {
Filip Pavlise63b30c2020-01-09 16:21:07 +0000330 assertThat(drawerClicks).isEqualTo(0)
331 assertThat(bodyClicks).isEqualTo(1)
332 }
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100333
Filip Pavlis6fdb2502020-04-02 14:41:32 +0100334 runOnUiThread {
Leland Richardsonfcf76b32020-05-13 16:58:59 -0700335 drawerState.value = DrawerState.Opened
Jelle Fresen11fc7062020-01-13 16:53:55 +0000336 }
337 sleep(100) // TODO(147586311): remove this sleep when opening the drawer triggers a wait
338
339 // Click on the bottom-center pixel of the drawer
340 findByTag("Drawer").doGesture {
341 val bounds = globalBounds
342 val centerX = bounds.width / 2
Ryan Mentley7865a632019-08-20 20:10:29 -0700343 val bottom = bounds.height - 1.px
344 sendClick(PxPosition(centerX, bottom))
Jelle Fresen11fc7062020-01-13 16:53:55 +0000345 }
346
347 assertThat(drawerClicks).isEqualTo(1)
348 assertThat(bodyClicks).isEqualTo(1)
Matvei Malkov7aa7fd22019-08-16 16:52:59 +0100349 }
Matvei Malkov2d37cbd2019-07-04 15:15:20 +0100350}