Skip to content

Commit 58fda6a

Browse files
Instrumented tests improvements
1 parent 78b6a81 commit 58fda6a

11 files changed

Lines changed: 153 additions & 94 deletions

.github/workflows/codeql-analysis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ jobs:
2828
distribution: 'zulu'
2929
java-version: '21'
3030
- name: Initialize CodeQL
31-
uses: github/codeql-action/init@v3
31+
uses: github/codeql-action/init@v4
3232
with:
3333
languages: ${{ matrix.language }}
3434
- name: Auto build
35-
uses: github/codeql-action/autobuild@v3
35+
uses: github/codeql-action/autobuild@v4
3636
- name: Perform CodeQL Analysis
37-
uses: github/codeql-action/analyze@v3
37+
uses: github/codeql-action/analyze@v4
3838
with:
3939
category: "/language:${{matrix.language}}"

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ dependencies {
6060
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.7.0'
6161
androidTestImplementation 'androidx.test.ext:junit-ktx:1.3.0'
6262
androidTestImplementation 'androidx.test:rules:1.7.0'
63+
androidTestImplementation 'org.assertj:assertj-core:3.27.6'
6364
}
6465

6566
android {

app/src/androidTest/kotlin/com/vrem/wifianalyzer/AccessPointsInstrumentedTest.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@ import androidx.test.espresso.action.ViewActions.swipeDown
2424
import androidx.test.espresso.assertion.ViewAssertions.matches
2525
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
2626
import androidx.test.espresso.matcher.ViewMatchers.withId
27-
import androidx.test.espresso.matcher.ViewMatchers.withText
2827
import org.hamcrest.Matchers.allOf
2928
import org.hamcrest.Matchers.anything
30-
import org.hamcrest.Matchers.containsString
3129

3230
internal class AccessPointsInstrumentedTest : Runnable {
3331
override fun run() {
@@ -55,7 +53,6 @@ internal class AccessPointsInstrumentedTest : Runnable {
5553
.atPosition(0)
5654
.perform(click())
5755
pauseShort()
58-
onView(withText(containsString("AndroidWifi"))).check(matches(isDisplayed()))
5956
dismissPopup()
6057
}
6158
}

app/src/androidTest/kotlin/com/vrem/wifianalyzer/ChannelAvailableInstrumentedTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import org.hamcrest.Matchers.allOf
2626

2727
internal class ChannelAvailableInstrumentedTest : Runnable {
2828
override fun run() {
29-
selectMenuItem(7, "Available Channels")
29+
selectMenuItem(R.id.nav_drawer_channel_available, "Available Channels")
3030
verifySections()
3131
pressBack()
3232
}

app/src/androidTest/kotlin/com/vrem/wifianalyzer/ConnectionInstrumentedTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
2323
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
2424
import androidx.test.espresso.matcher.ViewMatchers.withId
2525
import androidx.test.espresso.matcher.ViewMatchers.withText
26-
import org.hamcrest.Matchers.containsString
26+
import org.hamcrest.Matchers.matchesPattern
2727

2828
internal class ConnectionInstrumentedTest : Runnable {
2929
override fun run() {
@@ -38,7 +38,8 @@ internal class ConnectionInstrumentedTest : Runnable {
3838
}
3939

4040
private fun verifyIpAddressDisplayed() {
41-
onView(withText(containsString("10.0.2.16"))).check(matches(isDisplayed()))
41+
val ipv4Pattern = "\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b"
42+
onView(withText(matchesPattern(ipv4Pattern))).check(matches(isDisplayed()))
4243
}
4344

4445
private fun verifyConnectionPopup() {

app/src/androidTest/kotlin/com/vrem/wifianalyzer/FilterInstrumentedTest.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ internal class FilterInstrumentedTest : Runnable {
3535
verifyDialogAndReset()
3636
verifyWifiBandFilter()
3737
verifySsidFilter()
38-
resetFilters()
3938
}
4039

4140
private fun verifyDialogAndReset() {
@@ -63,9 +62,4 @@ internal class FilterInstrumentedTest : Runnable {
6362
onView(allOf(withId(R.id.action_filter), isDisplayed())).perform(click())
6463
onView(allOf(withId(android.R.id.button2), isDisplayed())).perform(scrollTo(), click())
6564
}
66-
67-
private fun resetFilters() {
68-
onView(allOf(withId(R.id.action_filter), isDisplayed())).perform(click())
69-
onView(allOf(withId(android.R.id.button2), isDisplayed())).perform(scrollTo(), click())
70-
}
7165
}

app/src/androidTest/kotlin/com/vrem/wifianalyzer/InstrumentedTestUtils.kt

Lines changed: 104 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -20,47 +20,39 @@ package com.vrem.wifianalyzer
2020
import android.view.InputDevice
2121
import android.view.MotionEvent
2222
import android.view.View
23-
import android.view.ViewGroup
2423
import androidx.appcompat.widget.Toolbar
2524
import androidx.recyclerview.widget.RecyclerView
2625
import androidx.test.espresso.Espresso.onView
26+
import androidx.test.espresso.Espresso.pressBack
27+
import androidx.test.espresso.UiController
2728
import androidx.test.espresso.ViewAction
2829
import androidx.test.espresso.action.GeneralClickAction
2930
import androidx.test.espresso.action.Press
3031
import androidx.test.espresso.action.Tap
3132
import androidx.test.espresso.action.ViewActions.click
3233
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
3334
import androidx.test.espresso.assertion.ViewAssertions.matches
35+
import androidx.test.espresso.contrib.DrawerActions
3436
import androidx.test.espresso.contrib.RecyclerViewActions
3537
import androidx.test.espresso.matcher.BoundedMatcher
3638
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
3739
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
3840
import 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
4142
import androidx.test.espresso.matcher.ViewMatchers.withId
4243
import androidx.test.espresso.matcher.ViewMatchers.withText
4344
import org.hamcrest.Description
4445
import org.hamcrest.Matcher
45-
import org.hamcrest.Matchers
4646
import 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+
*/
6456
internal 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+
*/
8399
internal 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+
*/
87107
internal 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+
*/
92116
internal 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+
*/
96123
internal 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+
*/
103136
internal 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+
*/
125159
internal 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+
*/
162173
internal 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+
}

app/src/androidTest/kotlin/com/vrem/wifianalyzer/MainInstrumentedTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
*/
1818
package com.vrem.wifianalyzer
1919

20+
import androidx.test.espresso.Espresso.pressBack
2021
import androidx.test.ext.junit.rules.ActivityScenarioRule
2122
import androidx.test.ext.junit.rules.activityScenarioRule
2223
import androidx.test.ext.junit.runners.AndroidJUnit4
2324
import androidx.test.filters.LargeTest
2425
import androidx.test.rule.GrantPermissionRule
26+
import org.junit.After
27+
import org.junit.Before
2528
import org.junit.Rule
2629
import org.junit.Test
2730
import org.junit.runner.RunWith
@@ -41,6 +44,23 @@ class MainInstrumentedTest {
4144
android.Manifest.permission.CHANGE_WIFI_STATE,
4245
)
4346

47+
@Before
48+
fun setUp() {
49+
returnToHome()
50+
resetFilters()
51+
resetScannerState()
52+
resetSettings()
53+
returnToHome()
54+
}
55+
56+
@After
57+
fun tearDown() {
58+
try {
59+
pressBack()
60+
} catch (_: Exception) {
61+
}
62+
}
63+
4464
@Test
4565
fun navigation() {
4666
NavigationInstrumentedTest().run()

0 commit comments

Comments
 (0)