1+ /*
2+ * Copyright (c) 2025 Olivier Patry
3+ *
4+ * Permission is hereby granted, free of charge, to any person obtaining
5+ * a copy of this software and associated documentation files (the "Software"),
6+ * to deal in the Software without restriction, including without limitation
7+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8+ * and/or sell copies of the Software, and to permit persons to whom the Software
9+ * is furnished to do so, subject to the following conditions:
10+ *
11+ * The above copyright notice and this permission notice shall be included in
12+ * all copies or substantial portions of the Software.
13+ *
14+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
16+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
20+ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+ */
22+
23+ package net.opatry.tasks.app.test.screenshot
24+
25+ import android.content.Context
26+ import androidx.appcompat.app.AppCompatDelegate
27+ import androidx.compose.ui.test.ExperimentalTestApi
28+ import androidx.compose.ui.test.assertIsDisplayed
29+ import androidx.compose.ui.test.hasTestTag
30+ import androidx.compose.ui.test.hasText
31+ import androidx.compose.ui.test.isDialog
32+ import androidx.compose.ui.test.junit4.createAndroidComposeRule
33+ import androidx.compose.ui.test.onNodeWithTag
34+ import androidx.compose.ui.test.onNodeWithText
35+ import androidx.compose.ui.test.performClick
36+ import androidx.compose.ui.test.performTextInput
37+ import androidx.test.platform.app.InstrumentationRegistry
38+ import androidx.test.uiautomator.UiDevice
39+ import kotlinx.coroutines.test.runTest
40+ import net.opatry.tasks.app.MainActivity
41+ import net.opatry.tasks.app.R
42+ import net.opatry.tasks.app.test.ScreenshotOnFailureRule
43+ import net.opatry.tasks.app.ui.component.TaskEditorBottomSheetTestTag.NOTES_FIELD
44+ import net.opatry.tasks.app.ui.component.TaskEditorBottomSheetTestTag.TITLE_FIELD
45+ import net.opatry.tasks.app.ui.component.TaskListScaffoldTestTag.ADD_TASK_FAB
46+ import net.opatry.tasks.app.ui.component.TasksColumnTestTag.COMPLETED_TASKS_TOGGLE
47+ import org.junit.Rule
48+ import org.junit.Test
49+ import java.io.File
50+
51+
52+ @OptIn(ExperimentalTestApi ::class )
53+ class StoreScreenshotTest {
54+
55+ @get:Rule
56+ val composeTestRule = createAndroidComposeRule<MainActivity >()
57+
58+ @get:Rule
59+ val screenshotOnFailureRule = ScreenshotOnFailureRule ()
60+
61+ private val targetContext: Context
62+ get() = InstrumentationRegistry .getInstrumentation().targetContext
63+
64+ private fun takeScreenshot (name : String ) {
65+ val instrumentation = InstrumentationRegistry .getInstrumentation()
66+ val outputDir = File (instrumentation.targetContext.cacheDir, " store_screenshots" ).also (File ::mkdirs)
67+ val outputFile = File (outputDir, " $name .png" )
68+ if (outputFile.exists()) {
69+ outputFile.delete()
70+ }
71+ UiDevice .getInstance(instrumentation)
72+ .takeScreenshot(outputFile)
73+ }
74+
75+ private fun pressBack () {
76+ // FIXME how to "press back" with ComposeTestRule (without Espresso)
77+ // UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressBack()
78+ // UI Automator doesn't work for navigation (but does for IME dismiss)
79+ composeTestRule.activity.onBackPressed()
80+ }
81+
82+ private fun dismissKeyboard () {
83+ UiDevice .getInstance(InstrumentationRegistry .getInstrumentation()).pressBack()
84+ }
85+
86+ private fun switchToNightMode (nightMode : Int ) {
87+ composeTestRule.activity.runOnUiThread {
88+ composeTestRule.activity.delegate.localNightMode = nightMode
89+ }
90+ composeTestRule.activityRule.scenario.recreate()
91+ composeTestRule.waitForIdle()
92+ }
93+
94+ /* *
95+ * This test should be executed with the `demo` flavor which stub content for store screenshots.
96+ */
97+ @Test
98+ fun storeScreenshotSequence () = runTest {
99+ val initialNightMode = composeTestRule.activity.delegate.localNightMode
100+
101+ composeTestRule.waitForIdle()
102+ takeScreenshot(" initial_screen" )
103+
104+ switchToNightMode(AppCompatDelegate .MODE_NIGHT_NO )
105+
106+ val defaultTaskTitle = targetContext.getString(R .string.demo_task_list_default)
107+ composeTestRule.waitUntilAtLeastOneExists(hasText(defaultTaskTitle))
108+ composeTestRule.onNodeWithText(defaultTaskTitle)
109+ .assertIsDisplayed()
110+
111+ val homeTaskTitle = targetContext.getString(R .string.demo_task_list_home)
112+ composeTestRule.onNodeWithText(homeTaskTitle)
113+ .assertIsDisplayed()
114+
115+ val groceriesTaskTitle = targetContext.getString(R .string.demo_task_list_groceries)
116+ composeTestRule.onNodeWithText(groceriesTaskTitle)
117+ .assertIsDisplayed()
118+
119+ val workTaskTitle = targetContext.getString(R .string.demo_task_list_work)
120+ composeTestRule.onNodeWithText(workTaskTitle)
121+ .assertIsDisplayed()
122+
123+ takeScreenshot(" task_lists_light" )
124+
125+ composeTestRule.onNodeWithText(defaultTaskTitle)
126+ .assertIsDisplayed()
127+ .performClick()
128+ val defaultTask1Title = targetContext.getString(R .string.demo_task_list_default_task1)
129+ composeTestRule.waitUntilAtLeastOneExists(hasText(defaultTask1Title))
130+ // FIXME unreliable, need to wait for something else?
131+ takeScreenshot(" my_tasks_light" )
132+
133+ composeTestRule.waitUntilExactlyOneExists(hasTestTag(ADD_TASK_FAB ))
134+ composeTestRule.onNodeWithTag(ADD_TASK_FAB )
135+ .assertIsDisplayed()
136+ .performClick()
137+ composeTestRule.waitUntilExactlyOneExists(isDialog())
138+
139+ composeTestRule.waitUntilExactlyOneExists(hasTestTag(TITLE_FIELD ))
140+ composeTestRule.onNodeWithTag(TITLE_FIELD )
141+ .performTextInput(" Wash the car 🧽" )
142+ composeTestRule.waitForIdle()
143+ dismissKeyboard()
144+
145+ composeTestRule.waitUntilExactlyOneExists(hasTestTag(NOTES_FIELD ))
146+ composeTestRule.onNodeWithTag(NOTES_FIELD )
147+ .performTextInput(" Keys are in the drawer" )
148+
149+ dismissKeyboard()
150+
151+ composeTestRule.waitForIdle()
152+ takeScreenshot(" add_task_light" )
153+
154+ // FIXME how to dismiss bottom sheet without clicking on the button? (press back somehow? tap outside?)
155+ // FIXME how to use Res strings from :tasks-app-shared?
156+ composeTestRule.onNodeWithText(" Cancel" )
157+ .assertIsDisplayed()
158+ .performClick()
159+ // go back
160+ pressBack()
161+ composeTestRule.waitUntilAtLeastOneExists(hasText(groceriesTaskTitle))
162+
163+ composeTestRule.onNodeWithText(groceriesTaskTitle)
164+ .assertIsDisplayed()
165+ .performClick()
166+ val groceriesTask1Title = targetContext.getString(R .string.demo_task_list_groceries_task1)
167+ composeTestRule.waitUntilAtLeastOneExists(hasText(groceriesTask1Title))
168+ composeTestRule.onNodeWithTag(COMPLETED_TASKS_TOGGLE )
169+ .assertIsDisplayed()
170+ .performClick()
171+ val groceriesTask3Title = targetContext.getString(R .string.demo_task_list_groceries_task3)
172+ composeTestRule.waitUntilAtLeastOneExists(hasText(groceriesTask3Title))
173+ takeScreenshot(" groceries_light" )
174+
175+ pressBack()
176+ composeTestRule.waitUntilAtLeastOneExists(hasText(workTaskTitle))
177+
178+ composeTestRule.onNodeWithText(workTaskTitle)
179+ .assertIsDisplayed()
180+ .performClick()
181+ val workTask1Title = targetContext.getString(R .string.demo_task_list_work_task1)
182+ composeTestRule.waitUntilAtLeastOneExists(hasText(workTask1Title))
183+ takeScreenshot(" work_light" )
184+
185+ pressBack()
186+ composeTestRule.waitUntilAtLeastOneExists(hasText(homeTaskTitle))
187+
188+ composeTestRule.onNodeWithText(homeTaskTitle)
189+ .assertIsDisplayed()
190+ .performClick()
191+ val homeTask1Title = targetContext.getString(R .string.demo_task_list_home_task1)
192+ composeTestRule.waitUntilAtLeastOneExists(hasText(homeTask1Title))
193+ takeScreenshot(" home_light" )
194+
195+ switchToNightMode(AppCompatDelegate .MODE_NIGHT_YES )
196+ composeTestRule.waitUntilAtLeastOneExists(hasText(homeTask1Title))
197+ takeScreenshot(" home_dark" )
198+ switchToNightMode(initialNightMode)
199+ }
200+ }
0 commit comments