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.e2e
24+
25+ import androidx.compose.ui.test.ExperimentalTestApi
26+ import androidx.compose.ui.test.assertIsDisplayed
27+ import androidx.compose.ui.test.hasTestTag
28+ import androidx.compose.ui.test.hasText
29+ import androidx.compose.ui.test.onNodeWithTag
30+ import androidx.compose.ui.test.onNodeWithText
31+ import androidx.compose.ui.test.performClick
32+ import androidx.compose.ui.test.performTextClearance
33+ import androidx.compose.ui.test.performTextInput
34+ import androidx.compose.ui.test.runAndroidComposeUiTest
35+ import androidx.compose.ui.test.waitUntilDoesNotExist
36+ import androidx.compose.ui.test.waitUntilExactlyOneExists
37+ import androidx.test.ext.junit.runners.AndroidJUnit4
38+ import net.opatry.tasks.app.MainActivity
39+ import net.opatry.tasks.app.ui.component.EmptyStatesTestTag.NO_TASKS_EMPTY_STATE
40+ import net.opatry.tasks.app.ui.component.EmptyStatesTestTag.NO_TASK_LISTS_EMPTY_STATE_CREATE_LIST_BUTTON
41+ import net.opatry.tasks.app.ui.component.TaskListScaffoldTestTag.ADD_TASK_FAB
42+ import net.opatry.tasks.app.ui.component.TasksColumnTestTag.ALL_TASKS_COMPLETED_EMPTY_STATE
43+ import net.opatry.tasks.app.ui.component.TasksColumnTestTag.COMPLETED_TASKS_TOGGLE
44+ import org.junit.Test
45+ import org.junit.runner.RunWith
46+ import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_ICON as COMPLETED_TASK_TOGGLE_ICON
47+ import net.opatry.tasks.app.ui.component.EditTextDialogTestTag.TEXT_FIELD as CREATE_TASK_LIST_DIALOG_TEXT_FIELD
48+ import net.opatry.tasks.app.ui.component.EditTextDialogTestTag.VALIDATE_BUTTON as CREATE_TASK_LIST_DIALOG_VALIDATE_BUTTON
49+ import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.MENU_ICON as REMAINING_TASK_MENU_ICON
50+ import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.TOGGLE_ICON as REMAINING_TASK_TOGGLE_ICON
51+ import net.opatry.tasks.app.ui.component.TaskEditorBottomSheetTestTag.NOTES_FIELD as TASK_NOTES_FIELD
52+ import net.opatry.tasks.app.ui.component.TaskEditorBottomSheetTestTag.TITLE_FIELD as TASK_TITLE_FIELD
53+ import net.opatry.tasks.app.ui.component.TaskEditorBottomSheetTestTag.VALIDATE_BUTTON as CREATE_TASK_SHEET_VALIDATE_BUTTON
54+ import net.opatry.tasks.app.ui.component.TaskMenuTestTag.DELETE as REMAINING_TASK_DELETE_MENU_ITEM
55+ import net.opatry.tasks.app.ui.screen.AuthorizationScreenTestTags.SKIP_BUTTON as AUTHORIZATION_SCREEN_SKIP_BUTTON
56+
57+ @OptIn(ExperimentalTestApi ::class )
58+ @RunWith(AndroidJUnit4 ::class )
59+ class TaskfolioE2ETest {
60+
61+ @Test
62+ fun testTaskfolioHappyPath () = runAndroidComposeUiTest<MainActivity > {
63+ // FIXME string resources from :tasks-app-shared?
64+ // FIXME replace testTag with string resources?
65+
66+ // skip authorization screen
67+ waitUntilExactlyOneExists(hasTestTag(AUTHORIZATION_SCREEN_SKIP_BUTTON ))
68+ onNodeWithTag(AUTHORIZATION_SCREEN_SKIP_BUTTON )
69+ .assertIsDisplayed()
70+ .performClick()
71+
72+ // create the first task list
73+ waitUntilExactlyOneExists(hasTestTag(NO_TASK_LISTS_EMPTY_STATE_CREATE_LIST_BUTTON ))
74+ onNodeWithTag(NO_TASK_LISTS_EMPTY_STATE_CREATE_LIST_BUTTON )
75+ .assertIsDisplayed()
76+ .performClick()
77+
78+ waitUntilExactlyOneExists(hasTestTag(CREATE_TASK_LIST_DIALOG_TEXT_FIELD ))
79+ onNodeWithTag(CREATE_TASK_LIST_DIALOG_TEXT_FIELD )
80+ .performTextClearance()
81+
82+ val listTitle = " My list ✅"
83+ onNodeWithTag(CREATE_TASK_LIST_DIALOG_TEXT_FIELD )
84+ .performTextInput(listTitle)
85+
86+ waitUntilExactlyOneExists(hasTestTag(CREATE_TASK_LIST_DIALOG_VALIDATE_BUTTON ))
87+ onNodeWithTag(CREATE_TASK_LIST_DIALOG_VALIDATE_BUTTON )
88+ .performClick()
89+
90+ // creating a list switches to the tasks screen automatically
91+ waitUntilExactlyOneExists(hasTestTag(NO_TASKS_EMPTY_STATE ))
92+ onNodeWithTag(NO_TASKS_EMPTY_STATE )
93+ .assertIsDisplayed()
94+ onNodeWithText(listTitle)
95+ .assertIsDisplayed()
96+
97+ // add a task to the list
98+ waitUntilExactlyOneExists(hasTestTag(ADD_TASK_FAB ))
99+ onNodeWithTag(ADD_TASK_FAB )
100+ .performClick()
101+
102+ val taskTitle = " My Wonderful task 🌈"
103+ waitUntilExactlyOneExists(hasTestTag(TASK_TITLE_FIELD ))
104+ onNodeWithTag(TASK_TITLE_FIELD )
105+ .performTextInput(taskTitle)
106+
107+ val taskNotes = " Some notes:\n • with a list\n • and some emojis 😊"
108+ waitUntilExactlyOneExists(hasTestTag(TASK_NOTES_FIELD ))
109+ onNodeWithTag(TASK_NOTES_FIELD )
110+ .performTextInput(taskNotes)
111+
112+ waitUntilExactlyOneExists(hasTestTag(CREATE_TASK_SHEET_VALIDATE_BUTTON ))
113+ onNodeWithTag(CREATE_TASK_SHEET_VALIDATE_BUTTON )
114+ .performClick()
115+
116+ waitUntilExactlyOneExists(hasText(taskTitle))
117+ onNodeWithText(taskTitle)
118+ .assertIsDisplayed()
119+ onNodeWithText(taskNotes)
120+ .assertIsDisplayed()
121+
122+ // mark the task as completed
123+ waitUntilExactlyOneExists(hasTestTag(REMAINING_TASK_TOGGLE_ICON ))
124+ onNodeWithTag(REMAINING_TASK_TOGGLE_ICON )
125+ .assertIsDisplayed()
126+ .performClick()
127+
128+ waitUntilExactlyOneExists(hasTestTag(ALL_TASKS_COMPLETED_EMPTY_STATE ))
129+ onNodeWithTag(ALL_TASKS_COMPLETED_EMPTY_STATE )
130+ .assertIsDisplayed()
131+ onNodeWithText(taskTitle)
132+ .assertDoesNotExist()
133+
134+ // expand completed tasks area
135+ waitUntilExactlyOneExists(hasTestTag(COMPLETED_TASKS_TOGGLE ))
136+ onNodeWithTag(COMPLETED_TASKS_TOGGLE )
137+ .assertIsDisplayed()
138+ .performClick()
139+
140+ waitUntilExactlyOneExists(hasTestTag(COMPLETED_TASK_TOGGLE_ICON ))
141+ onNodeWithText(taskTitle)
142+ .assertIsDisplayed()
143+
144+ // restore completed task as remaining task
145+ onNodeWithTag(COMPLETED_TASK_TOGGLE_ICON )
146+ .performClick()
147+
148+ waitUntilDoesNotExist(hasTestTag(COMPLETED_TASKS_TOGGLE ))
149+ onNodeWithTag(COMPLETED_TASKS_TOGGLE )
150+ .assertDoesNotExist()
151+
152+ onNodeWithTag(ALL_TASKS_COMPLETED_EMPTY_STATE )
153+ .assertDoesNotExist()
154+ onNodeWithTag(REMAINING_TASK_TOGGLE_ICON )
155+ .assertIsDisplayed()
156+ onNodeWithText(taskTitle)
157+ .assertIsDisplayed()
158+
159+ // delete the task
160+ onNodeWithTag(REMAINING_TASK_MENU_ICON )
161+ .performClick()
162+
163+ waitUntilExactlyOneExists(hasTestTag(REMAINING_TASK_DELETE_MENU_ITEM ))
164+ onNodeWithTag(REMAINING_TASK_DELETE_MENU_ITEM )
165+ .performClick()
166+
167+ waitUntilExactlyOneExists(hasTestTag(NO_TASKS_EMPTY_STATE ))
168+ onNodeWithTag(NO_TASKS_EMPTY_STATE )
169+ .assertIsDisplayed()
170+ }
171+ }
0 commit comments