Skip to content

Commit d3417dc

Browse files
committed
Add E2E test for happy path
1 parent d33d1cd commit d3417dc

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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

Comments
 (0)