@@ -20,47 +20,39 @@ package com.vrem.wifianalyzer
2020import android.view.InputDevice
2121import android.view.MotionEvent
2222import android.view.View
23- import android.view.ViewGroup
2423import androidx.appcompat.widget.Toolbar
2524import androidx.recyclerview.widget.RecyclerView
2625import androidx.test.espresso.Espresso.onView
26+ import androidx.test.espresso.Espresso.pressBack
27+ import androidx.test.espresso.UiController
2728import androidx.test.espresso.ViewAction
2829import androidx.test.espresso.action.GeneralClickAction
2930import androidx.test.espresso.action.Press
3031import androidx.test.espresso.action.Tap
3132import androidx.test.espresso.action.ViewActions.click
3233import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
3334import androidx.test.espresso.assertion.ViewAssertions.matches
35+ import androidx.test.espresso.contrib.DrawerActions
3436import androidx.test.espresso.contrib.RecyclerViewActions
3537import androidx.test.espresso.matcher.BoundedMatcher
3638import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
3739import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
3840import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
39- import androidx.test.espresso.matcher.ViewMatchers.withClassName
40- import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
41+ import androidx.test.espresso.matcher.ViewMatchers.isRoot
4142import androidx.test.espresso.matcher.ViewMatchers.withId
4243import androidx.test.espresso.matcher.ViewMatchers.withText
4344import org.hamcrest.Description
4445import org.hamcrest.Matcher
45- import org.hamcrest.Matchers
4646import org.hamcrest.Matchers.allOf
47- import org.hamcrest.TypeSafeMatcher
48-
49- internal class ChildAtPosition (
50- val parentMatcher : Matcher <View >,
51- val position : Int ,
52- ) : TypeSafeMatcher<View>() {
53- override fun describeTo (description : Description ) {
54- description.appendText(" Child at position $position in parent " )
55- parentMatcher.describeTo(description)
56- }
5747
58- override fun matchesSafely (view : View ): Boolean {
59- val parent = view.parent
60- return (parent is ViewGroup && parentMatcher.matches(parent) && view == parent.getChildAt(position))
61- }
62- }
48+ private const val PAUSE_1_SECOND = 1000L
49+ private const val PAUSE_20_SECONDS = 20000L
6350
51+ /* *
52+ * Returns a matcher that matches a Toolbar with the given title.
53+ * @param expectedTitle The expected toolbar title.
54+ * @return Matcher for Toolbar with the specified title.
55+ */
6456internal fun withToolbarTitle (expectedTitle : CharSequence ): Matcher <View > =
6557 object : BoundedMatcher <View , Toolbar >(Toolbar ::class .java) {
6658 override fun describeTo (description : Description ) {
@@ -70,36 +62,77 @@ internal fun withToolbarTitle(expectedTitle: CharSequence): Matcher<View> =
7062 override fun matchesSafely (toolbar : Toolbar ): Boolean = toolbar.title == expectedTitle
7163 }
7264
73- private const val SLEEP_1_SECOND = 1000
65+ /* *
66+ * Custom Espresso ViewAction to wait for a given duration.
67+ */
68+ fun waitFor (delay : Long ): ViewAction =
69+ object : ViewAction {
70+ override fun getConstraints () = isRoot()
7471
75- internal fun pauseShort () = pause( SLEEP_1_SECOND )
72+ override fun getDescription () = " Wait for $delay milliseconds. "
7673
77- private const val SLEEP_20_SECONDS = 20000
74+ override fun perform (
75+ uiController : UiController ,
76+ view : View ? ,
77+ ) {
78+ uiController.loopMainThreadForAtLeast(delay)
79+ }
80+ }
7881
79- internal fun pauseLong () = pause(SLEEP_20_SECONDS )
82+ /* *
83+ * Pauses the test execution for a short duration (1 second) using Espresso ViewAction.
84+ */
85+ internal fun pauseShort () {
86+ onView(isRoot()).perform(waitFor(PAUSE_1_SECOND ))
87+ }
8088
81- private fun pause (sleepTime : Int ) = runCatching { Thread .sleep(sleepTime.toLong()) }.getOrElse { it.printStackTrace() }
89+ /* *
90+ * Pauses the test execution for a long duration (20 seconds) using Espresso ViewAction.
91+ */
92+ internal fun pauseLong () {
93+ onView(isRoot()).perform(waitFor(PAUSE_20_SECONDS ))
94+ }
8295
96+ /* *
97+ * Verifies that the current connection view is displayed.
98+ */
8399internal fun verifyCurrentConnectionDisplayed () {
84100 onView(withId(R .id.connection)).check(matches(isDisplayed()))
85101}
86102
103+ /* *
104+ * Navigates to a bottom navigation item by its ID and pauses briefly.
105+ * @param navId The resource ID of the navigation item.
106+ */
87107internal fun navigateToBottomNav (navId : Int ) {
88108 onView(allOf(withId(navId), isDisplayed())).perform(click())
89109 pauseShort()
90110}
91111
112+ /* *
113+ * Verifies that the toolbar displays the expected title.
114+ * @param expectedTitle The expected toolbar title.
115+ */
92116internal fun verifyToolbarTitle (expectedTitle : String ) {
93117 onView(isAssignableFrom(Toolbar ::class .java)).check(matches(withToolbarTitle(expectedTitle)))
94118}
95119
120+ /* *
121+ * Dismisses a popup dialog with an "OK" button and verifies it is no longer displayed.
122+ */
96123internal fun dismissPopup () {
97124 onView(withText(" OK" )).check(matches(isDisplayed()))
98125 onView(withText(" OK" )).perform(click())
99126 pauseShort()
100127 onView(withText(" OK" )).check(doesNotExist())
101128}
102129
130+ /* *
131+ * Returns a ViewAction that clicks at a specific percentage position within a view.
132+ * @param xPercent The X position as a percentage (0.0 to 1.0).
133+ * @param yPercent The Y position as a percentage (0.0 to 1.0).
134+ * @return ViewAction that performs the click.
135+ */
103136internal fun clickAtPosition (
104137 xPercent : Float ,
105138 yPercent : Float ,
@@ -118,47 +151,25 @@ internal fun clickAtPosition(
118151 MotionEvent .BUTTON_PRIMARY ,
119152 )
120153
121- private const val NAVIGATION_DRAWER_BUTTON = 0
122- private const val NAVIGATION_DRAWER_ACTION = 1
123- private const val NAVIGATION_DRAWER_TAG = " Open navigation drawer"
124-
154+ /* *
155+ * Selects a navigation drawer menu item by its resource ID and verifies the toolbar title.
156+ * @param menuItemId The resource ID of the menu item in the drawer.
157+ * @param expectedTitle The expected toolbar title after selection.
158+ */
125159internal fun selectMenuItem (
126- menuItem : Int ,
160+ menuItemId : Int ,
127161 expectedTitle : String ,
128162) {
129- onView(
130- allOf(
131- withContentDescription(NAVIGATION_DRAWER_TAG ),
132- ChildAtPosition (
133- allOf(
134- withId(R .id.toolbar),
135- ChildAtPosition (
136- withClassName(Matchers .`is `(" com.google.android.material.appbar.AppBarLayout" )),
137- NAVIGATION_DRAWER_BUTTON ,
138- ),
139- ),
140- NAVIGATION_DRAWER_ACTION ,
141- ),
142- isDisplayed(),
143- ),
144- ).check(matches(isDisplayed())).perform(click())
145-
146- onView(
147- allOf(
148- ChildAtPosition (
149- allOf(
150- withId(com.google.android.material.R .id.design_navigation_view),
151- ChildAtPosition (withId(R .id.nav_drawer), NAVIGATION_DRAWER_BUTTON ),
152- ),
153- menuItem,
154- ),
155- isDisplayed(),
156- ),
157- ).check(matches(isDisplayed())).perform(click())
158-
163+ onView(withId(R .id.drawer_layout)).perform(DrawerActions .open())
164+ onView(allOf(withId(menuItemId), isDisplayed())).check(matches(isDisplayed())).perform(click())
159165 onView(isAssignableFrom(Toolbar ::class .java)).check(matches(withToolbarTitle(expectedTitle)))
160166}
161167
168+ /* *
169+ * Scrolls to a specific item in a RecyclerView and verifies it is displayed.
170+ * @param text The text of the item to scroll to.
171+ * @param recyclerViewId The resource ID of the RecyclerView (default is preference recycler view).
172+ */
162173internal fun scrollToAndVerify (
163174 text : String ,
164175 recyclerViewId : Int = androidx.preference.R .id.recycler_view,
@@ -167,3 +178,37 @@ internal fun scrollToAndVerify(
167178 .perform(RecyclerViewActions .scrollTo<RecyclerView .ViewHolder >(hasDescendant(withText(text))))
168179 onView(withText(text)).check(matches(isDisplayed()))
169180}
181+
182+ /* *
183+ * Resets the WiFiAnalyzer filters to default.
184+ */
185+ internal fun resetFilters () {
186+ onView(allOf(withId(R .id.action_filter), isDisplayed())).perform(click())
187+ onView(allOf(withId(android.R .id.button2), isDisplayed())).perform(click())
188+ }
189+
190+ /* *
191+ * Ensures the scanner is running (pause and resume to reset state).
192+ */
193+ internal fun resetScannerState () {
194+ onView(withId(R .id.action_scanner)).check(matches(isDisplayed()))
195+ onView(withId(R .id.action_scanner)).perform(click()) // Pause if running
196+ onView(withId(R .id.action_scanner)).perform(click()) // Resume
197+ }
198+
199+ /* *
200+ * Resets the app settings to default using the Settings screen.
201+ */
202+ internal fun resetSettings () {
203+ selectMenuItem(R .id.nav_drawer_settings, " Settings" )
204+ scrollToAndVerify(" Reset" )
205+ onView(withText(" Reset" )).perform(click())
206+ pressBack()
207+ }
208+
209+ /* *
210+ * Navigates to the Access Points (home) screen.
211+ */
212+ internal fun returnToHome () {
213+ navigateToBottomNav(R .id.nav_bottom_access_points)
214+ }
0 commit comments