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+
24+ import android.content.Context
25+ import androidx.appcompat.app.AppCompatDelegate
26+ import androidx.compose.ui.test.ExperimentalTestApi
27+ import androidx.compose.ui.test.assertIsDisplayed
28+ import androidx.compose.ui.test.hasTestTag
29+ import androidx.compose.ui.test.hasText
30+ import androidx.compose.ui.test.isDialog
31+ import androidx.compose.ui.test.junit4.createAndroidComposeRule
32+ import androidx.compose.ui.test.onNodeWithTag
33+ import androidx.compose.ui.test.onNodeWithText
34+ import androidx.compose.ui.test.performClick
35+ import androidx.compose.ui.test.performTextInput
36+ import androidx.test.platform.app.InstrumentationRegistry
37+ import androidx.test.uiautomator.UiDevice
38+ import kotlinx.coroutines.test.runTest
39+ import net.opatry.tasks.app.MainActivity
40+ import net.opatry.tasks.app.R
41+ import net.opatry.tasks.app.ui.component.TaskListScaffoldTestTag.ADD_TASK_FAB
42+ import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASKS_TOGGLE
43+ import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.TASK_NOTES_FIELD
44+ import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.TASK_TITLE_FIELD
45+ import org.junit.Rule
46+ import org.junit.Test
47+ import java.io.File
48+
49+
50+ @OptIn(ExperimentalTestApi ::class )
51+ class StoreScreenshotTest {
52+
53+ @get:Rule
54+ val composeTestRule = createAndroidComposeRule<MainActivity >()
55+
56+ @get:Rule
57+ val screenshotOnFailureRule = ScreenshotOnFailureRule ()
58+
59+ private val targetContext: Context
60+ get() = InstrumentationRegistry .getInstrumentation().targetContext
61+
62+ private fun takeScreenshot (name : String ) {
63+ val instrumentation = InstrumentationRegistry .getInstrumentation()
64+ val outputDir = File (instrumentation.targetContext.cacheDir, " store_screenshots" ).also (File ::mkdirs)
65+ val outputFile = File (outputDir, " $name .png" )
66+ if (outputFile.exists()) {
67+ outputFile.delete()
68+ }
69+ UiDevice .getInstance(instrumentation)
70+ .takeScreenshot(outputFile)
71+ }
72+
73+ private fun pressBack () {
74+ // FIXME how to "press back" with ComposeTestRule (without Espresso)
75+ // UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).pressBack()
76+ // UI Automator doesn't work for navigation (but does for IME dismiss)
77+ composeTestRule.activity.onBackPressed()
78+ }
79+
80+ private fun dismissKeyboard () {
81+ UiDevice .getInstance(InstrumentationRegistry .getInstrumentation()).pressBack()
82+ }
83+
84+ private fun switchToNightMode (nightMode : Int ) {
85+ composeTestRule.activity.runOnUiThread {
86+ composeTestRule.activity.delegate.localNightMode = nightMode
87+ }
88+ composeTestRule.activityRule.scenario.recreate()
89+ composeTestRule.waitForIdle()
90+ }
91+
92+ /* *
93+ * This test should be executed with the `demo` flavor which stub content for store screenshots.
94+ */
95+ @Test
96+ fun storeScreenshotSequence () = runTest {
97+ val initialNightMode = composeTestRule.activity.delegate.localNightMode
98+
99+ composeTestRule.waitForIdle()
100+ takeScreenshot(" initial_screen" )
101+
102+ switchToNightMode(AppCompatDelegate .MODE_NIGHT_NO )
103+
104+ val defaultTaskTitle = targetContext.getString(R .string.demo_task_list_default)
105+ composeTestRule.waitUntilAtLeastOneExists(hasText(defaultTaskTitle))
106+ composeTestRule.onNodeWithText(defaultTaskTitle)
107+ .assertIsDisplayed()
108+
109+ val homeTaskTitle = targetContext.getString(R .string.demo_task_list_home)
110+ composeTestRule.onNodeWithText(homeTaskTitle)
111+ .assertIsDisplayed()
112+
113+ val groceriesTaskTitle = targetContext.getString(R .string.demo_task_list_groceries)
114+ composeTestRule.onNodeWithText(groceriesTaskTitle)
115+ .assertIsDisplayed()
116+
117+ val workTaskTitle = targetContext.getString(R .string.demo_task_list_work)
118+ composeTestRule.onNodeWithText(workTaskTitle)
119+ .assertIsDisplayed()
120+
121+ takeScreenshot(" task_lists_light" )
122+
123+ composeTestRule.onNodeWithText(defaultTaskTitle)
124+ .assertIsDisplayed()
125+ .performClick()
126+ val defaultTask1Title = targetContext.getString(R .string.demo_task_list_default_task1)
127+ composeTestRule.waitUntilAtLeastOneExists(hasText(defaultTask1Title))
128+ // FIXME unreliable, need to wait for something else?
129+ takeScreenshot(" my_tasks_light" )
130+
131+ composeTestRule.waitUntilExactlyOneExists(hasTestTag(ADD_TASK_FAB ))
132+ composeTestRule.onNodeWithTag(ADD_TASK_FAB )
133+ .assertIsDisplayed()
134+ .performClick()
135+ composeTestRule.waitUntilExactlyOneExists(isDialog())
136+
137+ composeTestRule.waitUntilExactlyOneExists(hasTestTag(TASK_TITLE_FIELD ))
138+ composeTestRule.onNodeWithTag(TASK_TITLE_FIELD )
139+ .performTextInput(" Wash the car 🧽" )
140+ composeTestRule.waitForIdle()
141+ dismissKeyboard()
142+
143+ composeTestRule.waitUntilExactlyOneExists(hasTestTag(TASK_NOTES_FIELD ))
144+ composeTestRule.onNodeWithTag(TASK_NOTES_FIELD )
145+ .performTextInput(" Keys are in the drawer" )
146+
147+ dismissKeyboard()
148+
149+ composeTestRule.waitForIdle()
150+ takeScreenshot(" add_task_light" )
151+
152+ // FIXME how to dismiss bottom sheet without clicking on the button? (press back somehow? tap outside?)
153+ // FIXME how to use Res strings from :tasks-app-shared?
154+ composeTestRule.onNodeWithText(" Cancel" )
155+ .assertIsDisplayed()
156+ .performClick()
157+ // go back
158+ pressBack()
159+ composeTestRule.waitUntilAtLeastOneExists(hasText(groceriesTaskTitle))
160+
161+ composeTestRule.onNodeWithText(groceriesTaskTitle)
162+ .assertIsDisplayed()
163+ .performClick()
164+ val groceriesTask1Title = targetContext.getString(R .string.demo_task_list_groceries_task1)
165+ composeTestRule.waitUntilAtLeastOneExists(hasText(groceriesTask1Title))
166+ composeTestRule.onNodeWithTag(COMPLETED_TASKS_TOGGLE )
167+ .assertIsDisplayed()
168+ .performClick()
169+ val groceriesTask3Title = targetContext.getString(R .string.demo_task_list_groceries_task3)
170+ composeTestRule.waitUntilAtLeastOneExists(hasText(groceriesTask3Title))
171+ takeScreenshot(" groceries_light" )
172+
173+ pressBack()
174+ composeTestRule.waitUntilAtLeastOneExists(hasText(workTaskTitle))
175+
176+ composeTestRule.onNodeWithText(workTaskTitle)
177+ .assertIsDisplayed()
178+ .performClick()
179+ val workTask1Title = targetContext.getString(R .string.demo_task_list_work_task1)
180+ composeTestRule.waitUntilAtLeastOneExists(hasText(workTask1Title))
181+ takeScreenshot(" work_light" )
182+
183+ pressBack()
184+ composeTestRule.waitUntilAtLeastOneExists(hasText(homeTaskTitle))
185+
186+ composeTestRule.onNodeWithText(homeTaskTitle)
187+ .assertIsDisplayed()
188+ .performClick()
189+ val homeTask1Title = targetContext.getString(R .string.demo_task_list_home_task1)
190+ composeTestRule.waitUntilAtLeastOneExists(hasText(homeTask1Title))
191+ takeScreenshot(" home_light" )
192+
193+ switchToNightMode(AppCompatDelegate .MODE_NIGHT_YES )
194+ composeTestRule.waitUntilAtLeastOneExists(hasText(homeTask1Title))
195+ takeScreenshot(" home_dark" )
196+ switchToNightMode(initialNightMode)
197+ }
198+ }
0 commit comments