Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.test.core.app.ApplicationProvider
import com.loosecannon.servicetag.MainActivity
import com.loosecannon.servicetag.core.model.AssetId
import com.loosecannon.servicetag.core.usecase.AssetCommand
import com.loosecannon.servicetag.l10n.dateFieldText
import com.loosecannon.servicetag.ui.asset.NO_PARENT
import kotlinx.coroutines.runBlocking
import org.junit.Before
Expand Down Expand Up @@ -286,7 +287,7 @@ class AssetModelDeviceProofTest {

// The date field opens on today, which the user may then backdate.
rule.awaitText("Retire this asset?")
rule.onNodeWithText(LocalDate.now().toString()).assertIsDisplayed()
rule.onNodeWithText(dateFieldText(LocalDate.now().toString())).assertIsDisplayed()
rule.onNodeWithText("Retire").performClick()

// The offer comes after the write, so declining it leaves the asset retired.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.ui.test.performTextReplacement
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import com.loosecannon.servicetag.MainActivity
import com.loosecannon.servicetag.l10n.dateFieldText
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
Expand Down Expand Up @@ -452,8 +453,9 @@ private fun ComposeTestRule.logWaterTest(
awaitTag("value-ph")
if (on != null) {
// Backdating is ordinary (§4): the date is a plain field holding today until it is changed.
onNode(hasSetTextAction() and hasText(LocalDate.now().toString()))
.performTextReplacement(on.toString())
// #102: the field draws and reads the phone's own order, so the day is found and typed that way.
onNode(hasSetTextAction() and hasText(dateFieldText(LocalDate.now().toString())))
.performTextReplacement(dateFieldText(on.toString()))
}
onNodeWithTag("value-ph").performTextReplacement(ph)
entryList().performScrollToNode(hasTestTag("value-free_chlorine"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ class AssetEditorSeasonAndHealthTest {
rule.awaitText("Snowblower")
rule.onNodeWithText(SAME_DATES_EVERY_YEAR).performScrollTo().assertIsSelected()
field(SEASON_STARTS).performScrollTo()
.assert(SemanticsMatcher.expectValue(SemanticsProperties.EditableText, AnnotatedString("11-01")))
.assert(SemanticsMatcher.expectValue(SemanticsProperties.EditableText, AnnotatedString(monthDayFieldText("11-01"))))
rule.onAllNodesWithText("Off means this asset is only in use", substring = true).assertCountEquals(0)
rule.onAllNodesWithText("Set both season dates or neither").assertCountEquals(0)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.loosecannon.servicetag.core.model.AssetId
import com.loosecannon.servicetag.core.model.OperationalCondition
import com.loosecannon.servicetag.core.usecase.AssetCommand
import com.loosecannon.servicetag.di.AppGraph
import com.loosecannon.servicetag.l10n.dateFieldText
import com.loosecannon.servicetag.ui.app
import com.loosecannon.servicetag.ui.awaitText
import com.loosecannon.servicetag.ui.clearInstall
Expand Down Expand Up @@ -88,7 +89,7 @@ class ChangeConditionSheetTest {
rule.onAllNodes(isSelected()).assertCountEquals(0)
rule.onNodeWithText("Save condition").assertIsNotEnabled()
field("What is wrong? (optional)").assertIsDisplayed()
field("When did this change?").assert(hasText(LocalDate.now().toString()))
field("When did this change?").assert(hasText(dateFieldText(LocalDate.now().toString())))

rule.onNodeWithText("Down").performClick()
field("What is wrong? (optional)").performTextInput("Will not start")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.loosecannon.servicetag.contacts.ContactRow
import com.loosecannon.servicetag.contacts.ContactRowQuery
Expand Down Expand Up @@ -313,13 +317,19 @@ class LendingSectionDeviceTest {
rule.onNodeWithText(CHOOSE_FROM_CONTACTS).performClick()
rule.awaitText("Sample Borrower")
field(DUE_BACK).performScrollTo().performTextReplacement(due.toString())
// Typing the date raised the soft keyboard, and the platform gives Back to a showing keyboard
// before any window: left up, it can be carried over the rationale and swallow the key under
// test (the gate's logcat: the IME re-shown over the dialog, the key spent hiding it). The
// keyboard is put away first, as a person does before reading the question.
Espresso.closeSoftKeyboard()
rule.onNodeWithText(ONCE).performScrollTo().performClick()
rule.onNodeWithText(SAVE_LOAN).performScrollTo().performClick()
rule.awaitText(LOAN_NOTIFICATION_RATIONALE)
val atTheQuestion = loans().single()

// A key to the focused dialog window, as a gesture is: never the activity's own dispatcher.
Espresso.pressBack()
// Espresso injects it only once the dialog's own window holds focus.
onView(isRoot()).inRoot(RootMatchers.isDialog()).perform(pressBack())
rule.waitUntil(WAIT_MS) { done.get() == 1 }
rule.waitForIdle()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,21 @@ class GroupScreensTest {

private fun openMaintenance() {
rule.onNode(hasText("Maintenance") and hasClickAction()).performClick()
rule.awaitText("Maintenance groups")
rule.awaitText(GROUPS_SECTION)
}

/**
* #103 (1.7.1): the groups left the tab for their own pushed list, so the way to a group is the
* person's way — the Maintenance tab, then the grouped section's "Maintenance groups" row, then the
* group's row on the list that opens. The list draws once the store has answered, which is what
* waiting for [group]'s row waits for — by its exact text, since the tab's due row carries the
* same name as its subtitle.
*/
private fun openGroupList(group: String = "North run") {
rule.onNode(hasTextExactly(GROUPS_SECTION) and hasClickAction()).performScrollTo().performClick()
rule.waitUntil(WAIT_MS) {
rule.onAllNodes(hasTextExactly(group) and hasClickAction()).fetchSemanticsNodes().isNotEmpty()
}
}

private fun openAssets() {
Expand All @@ -144,10 +158,11 @@ class GroupScreensTest {
@Test fun theListOpensAGroupAndAMemberRowOpensThatAssetsOwnScreen() {
aStoreWithGroups()
openMaintenance()
openGroupList()

// `hasTextExactly` and not `hasText`: the group's name is also the **subtitle** of its
// schedule's due row, which is clickable too and opens the schedule. A list row says the
// group's name and nothing else, which is what tells the two apart.
// schedule's due row on the tab, which is clickable too and opens the schedule. A list row
// says the group's name and nothing else, which is what tells the two apart.
rule.onNode(hasTextExactly("North run") and hasClickAction()).performScrollTo().performClick()

rule.awaitText("Maintenance group")
Expand Down Expand Up @@ -201,6 +216,7 @@ class GroupScreensTest {
@Test fun aMemberIsSoftRemovedAndReAddedWithTheListReadAtEachStep() {
aStoreWithGroups()
openMaintenance()
openGroupList("South run")
rule.onNode(hasTextExactly("South run") and hasClickAction()).performScrollTo().performClick()

rule.awaitText("Maintenance group")
Expand Down Expand Up @@ -249,6 +265,7 @@ class GroupScreensTest {
aStoreWithGroups()
openMaintenance()
rule.awaitText("Head check")
openGroupList()

rule.onNode(hasTextExactly("North run") and hasClickAction()).performScrollTo().performClick()
rule.awaitText("Maintenance group")
Expand All @@ -257,9 +274,16 @@ class GroupScreensTest {

rule.onNodeWithContentDescription("Back").performClick()

// Still listed, and marked; its round has left the due lists entirely.
// Still listed, and marked.
rule.awaitText("North run")
rule.awaitText("ARCHIVED")

// And its round has left the due lists entirely: back on the tab, the sprinkler's own
// schedule is listed and the group's is not. The tab's lists are read live, so the wait is
// for the archived round to leave them; the asset's own schedule says they are drawn.
rule.onNodeWithContentDescription("Back").performClick()
rule.awaitText("Nozzle clean")
rule.waitUntil(WAIT_MS) { rule.onAllNodesWithText("Head check").fetchSemanticsNodes().isEmpty() }
rule.onAllNodesWithText("Head check").assertCountEquals(0)

val stored = runBlocking { app.graph.groups.all().single { it.name == "North run" } }
Expand All @@ -284,6 +308,7 @@ class GroupScreensTest {
@Test fun theRoundsChecklistCompletesTheSelectedMemberThroughTheRatifiedAction() {
aStoreWithGroups()
openMaintenance()
openGroupList()
rule.onNode(hasTextExactly("North run") and hasClickAction()).performScrollTo().performClick()

rule.awaitText("1 of 3 complete")
Expand Down Expand Up @@ -327,6 +352,7 @@ class GroupScreensTest {
@Test fun theSchedulesSectionOffersCreationOnlyWhileTheGroupHasAMember() {
aStoreWithGroups()
openMaintenance()
openGroupList()
rule.onNode(hasTextExactly("North run") and hasClickAction()).performScrollTo().performClick()
rule.awaitText("Maintenance group")

Expand All @@ -339,9 +365,12 @@ class GroupScreensTest {
rule.awaitText("This applies to")
rule.awaitText("A maintenance group")

// And the group with nobody in it offers nothing to tap.
// And the group with nobody in it offers nothing to tap: back on the group list.
rule.onNodeWithContentDescription("Cancel").performClick()
rule.onNodeWithContentDescription("Back").performClick()
rule.waitUntil(WAIT_MS) {
rule.onAllNodes(hasTextExactly("Emptied run") and hasClickAction()).fetchSemanticsNodes().isNotEmpty()
}
rule.onNode(hasTextExactly("Emptied run") and hasClickAction()).performScrollTo().performClick()
rule.awaitText("Maintenance group")
// The ratified empty state stands in for the schedules it has none of.
Expand All @@ -351,4 +380,8 @@ class GroupScreensTest {

private fun assetIdOf(name: String): String =
runBlocking { app.graph.assets.all().single { it.name == name }.id.value }

private companion object {
const val WAIT_MS = 10_000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.loosecannon.servicetag.MainActivity
import com.loosecannon.servicetag.R
import com.loosecannon.servicetag.core.model.CompletionMode
import com.loosecannon.servicetag.core.model.MaintenanceSchedule
import com.loosecannon.servicetag.core.model.RecurrenceUnit
Expand All @@ -24,6 +25,7 @@ import com.loosecannon.servicetag.core.model.ScheduleTarget
import com.loosecannon.servicetag.core.model.ServicePolicy
import com.loosecannon.servicetag.core.model.TimeBasis
import com.loosecannon.servicetag.core.usecase.AssetCommand
import com.loosecannon.servicetag.l10n.localized
import com.loosecannon.servicetag.ui.app
import com.loosecannon.servicetag.ui.awaitText
import com.loosecannon.servicetag.ui.clearInstall
Expand Down Expand Up @@ -94,8 +96,9 @@ class ReminderHealthScreenTest {
// #103: Settings › Utilities › Reminder health. The row sits below the fold on a settings
// screen, and a node that is in the tree but off screen takes a click that goes nowhere,
// which reads exactly like a route that failed to open: scroll to it, the way a person would.
// The section heading is drawn in capitals (`SectionHeader`), so it is awaited as drawn.
rule.onNodeWithContentDescription("Settings").performClick()
rule.awaitText("Utilities")
rule.awaitText(localized(R.string.settings_utilities).uppercase())
rule.onNode(hasText(REMINDER_HEALTH_TITLE) and hasClickAction()).performScrollTo().performClick()
rule.awaitText(ALARM_FINDING)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.loosecannon.servicetag.core.usecase.AssetCommand
import com.loosecannon.servicetag.core.usecase.AttachmentResult
import com.loosecannon.servicetag.core.usecase.EventCommand
import com.loosecannon.servicetag.core.usecase.ServiceCaseCommand
import com.loosecannon.servicetag.l10n.dateFieldText
import com.loosecannon.servicetag.ui.app
import com.loosecannon.servicetag.ui.awaitText
import com.loosecannon.servicetag.ui.clearInstall
Expand Down Expand Up @@ -171,7 +172,7 @@ class ServiceCaseScreensTest {
rule.onNodeWithText(ADD_UPDATE).performScrollTo().performClick()
rule.awaitText(SAVE_UPDATE)
field("Date").assertIsDisplayed()
rule.onNode(hasSetTextAction() and hasText(today)).assertIsDisplayed()
rule.onNode(hasSetTextAction() and hasText(dateFieldText(today))).assertIsDisplayed()
field("Time").assertIsDisplayed()
field(WHAT_HAPPENED).assertIsDisplayed()
listOf(STATUS_SENT_OUT, STATUS_AT_THE_SERVICE_CENTER, STATUS_RETURNED, STATUS_CLOSED, STATUS_CANCELLED).forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import org.junit.Test

private const val TOKEN = "ABCD2345"

/** What `InputStream.read()` returns at the end of the stream. */
private const val END_OF_STREAM = -1

/**
* 1.1.0 (#46) — the listener itself, over a real loopback socket, on the JVM.
*
Expand Down Expand Up @@ -161,12 +164,15 @@ class LoopbackApiServerTest {

server.stop()
assertEquals(0, server.boundPort)
try {
val first = try {
Socket("127.0.0.1", port).use { it.getInputStream().read() }
fail("the port is still answering after stop()")
} catch (e: IOException) {
// Refused, which is the whole point of the lifecycle binding.
END_OF_STREAM
}
// A runner's stack can accept the connect and close it straight away: end of stream with no byte is nobody
// answering. One byte is an answer.
if (first != END_OF_STREAM) fail("the port is still answering after stop()")
}

/** What the screen's third line counts: answers given, refusals included. */
Expand Down