Skip to content

Commit

Permalink
Move UI State Management variables from ClockPageViewModel to ClockPa…
Browse files Browse the repository at this point in the history
…geViewModelState object (#24)

* Create ClockPageViewModelState class, move mutable state variables and function declarations into there. Some small cleanup.

* Remove ClockPage component's dependency on ClockPageViewModel. Display ClockPage composable preview again!

* Add doc comments for ClockPageViewModelState

* Create default ClockPage component test

* Move clockButtonEnabled and task dropdown logic to ClockPageViewModelState class. Create unit tests.

* Move countdown text change components and functions to ClockPageViewModelState. Write unit tests for text changes.

* Remove currCountDownSeconds and countDownEndTime from ClockPageViewModelState object

* Add testTags to components, wrote some ClockPage automated compose tests.

* Add default strings to EditTimerTextField, reenable compose layout test, write instrumented test for count down

* Add count down instrumented tests

* Add divider to EditTimerTextField and string resources
  • Loading branch information
NicksPatties authored Aug 9, 2022
1 parent 3b3833b commit 61679ad
Show file tree
Hide file tree
Showing 14 changed files with 753 additions and 313 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
testImplementation "androidx.room:room-testing:$room_version"
testImplementation "pl.pragmatists:JUnitParams:1.1.1"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.nickspatties.timeclock.ui.pages

import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.platform.app.InstrumentationRegistry
import com.nickspatties.timeclock.R
import com.nickspatties.timeclock.ui.theme.TimeClockTheme
import com.nickspatties.timeclock.ui.viewmodel.ClockPageViewModelState
import org.junit.Rule
import org.junit.Test

class ClockPageTest {

private val context = InstrumentationRegistry.getInstrumentation().targetContext

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun countUp_defaultConfiguration() {
composeTestRule.setContent {
TimeClockTheme {
val fakeViewModelState = ClockPageViewModelState()
ClockPage(viewModelState = fakeViewModelState)
}
}
composeTestRule.onNodeWithTag("TaskTextField").assertIsEnabled()
composeTestRule.onNodeWithTag("StartTimerButton")
.assertIsNotEnabled()
.assertTextEquals(context.getString(R.string.start))
}

@Test
fun countUp_StartTimerButtonEnabledWhenTextIsInTaskTextField() {
val testString = context.getString(R.string.start)
composeTestRule.setContent {
TimeClockTheme {
val fakeViewModelState = ClockPageViewModelState()
ClockPage(viewModelState = fakeViewModelState)
}
}
composeTestRule.onNodeWithTag("TaskTextField").assertIsEnabled()
composeTestRule.onNodeWithTag("TaskTextField").performTextInput("programming")
composeTestRule.onNodeWithTag("StartTimerButton")
.assertIsEnabled()
.assertTextEquals(testString)
}

@Test
fun taskNameDropdown_dropdownAppearsAndTaskFillsInWhenLabelIsClicked() {
composeTestRule.setContent {
TimeClockTheme {
ClockPage(viewModelState =
ClockPageViewModelState(
autofillTaskNames = setOf(
"programming",
"reading"
)
)
)
}
}
composeTestRule.onNodeWithTag("TaskTextField").performTextInput("pro")
composeTestRule.onNodeWithTag("DropdownMenuItem_programming").performClick()
composeTestRule.onNodeWithTag("TaskTextField", useUnmergedTree = true)
.assertTextEquals("programming")
}

@Test
fun countDown_defaultConfiguration() {
composeTestRule.setContent {
TimeClockTheme {
ClockPage(viewModelState = ClockPageViewModelState(
countDownTimerEnabled = true
))
}
}
composeTestRule.onNodeWithTag("TaskTextField").assertIsEnabled()
composeTestRule.onNodeWithTag("TimerTextField_Hours").assertIsEnabled()
composeTestRule.onNodeWithTag("TimerTextField_Minutes").assertIsEnabled()
composeTestRule.onNodeWithTag("TimerTextField_Seconds").assertIsEnabled()
composeTestRule.onNodeWithTag("StartTimerButton")
.assertIsNotEnabled()
.assertTextEquals(context.getString(R.string.start))
}

@Test
fun countDown_StartTimerButtonStillDisabledIfTaskNameIsNotEmptyAndTimeIsZero() {
composeTestRule.setContent {
TimeClockTheme {
ClockPage(viewModelState = ClockPageViewModelState(
countDownTimerEnabled = true
))
}
}
composeTestRule.onNodeWithTag("TaskTextField").performTextInput("programming")
composeTestRule.onNodeWithTag("StartTimerButton").assertIsNotEnabled()
}

@Test
fun countDown_StartTimerButtonStillDisabledIfTaskNameIsEmptyTimeIsNotZero() {
composeTestRule.setContent {
TimeClockTheme {
ClockPage(viewModelState = ClockPageViewModelState(
countDownTimerEnabled = true
))
}
}
composeTestRule.onNodeWithTag("TimerTextField_Minutes").performTextInput("1")
composeTestRule.onNodeWithTag("StartTimerButton").assertIsNotEnabled()
}

@Test
fun countDown_StartTimerButtonEnabledIfTaskNameIsNotEmptyAndTimeIsNonZero() {
composeTestRule.setContent {
TimeClockTheme {
ClockPage(viewModelState = ClockPageViewModelState(
countDownTimerEnabled = true
))
}
}
composeTestRule.onNodeWithTag("TaskTextField").performTextInput("programming")
composeTestRule.onNodeWithTag("TimerTextField_Minutes").performTextInput("1")
composeTestRule.onNodeWithTag("StartTimerButton").assertIsEnabled()
}
}
6 changes: 4 additions & 2 deletions app/src/main/java/com/nickspatties/timeclock/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ fun NavigationComponent(
navController: NavHostController,
startDestination: String
) {
val clockPageViewModel = viewModel.clockPage
// observe changes on autofillTaskNames to allow filteredTaskNames to function properly
viewModel.clockPage.autofillTaskNames.observeAsState()
val clockPageViewModelState = viewModel.clockPage.state

val listPageViewModel = viewModel.listPage
val groupedEvents = listPageViewModel.groupedEventsByDate.observeAsState().value
Expand Down Expand Up @@ -170,7 +172,7 @@ fun NavigationComponent(
) {
composable(clockRoute) {
ClockPage(
viewModel = clockPageViewModel
viewModelState = clockPageViewModelState
)
}
composable(listRoute) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,78 +6,90 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import com.nickspatties.timeclock.ui.viewmodel.ClockPageViewModel
import com.nickspatties.timeclock.R

const val TAG = "EditTimerTextField"

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun EditTimerTextField(
// TODO: just pass TextFieldValues into this component. No need to pass the current seconds down
viewModel: ClockPageViewModel,
hoursTextFieldValue: TextFieldValue = TextFieldValue(),
minutesTextFieldValue: TextFieldValue = TextFieldValue(),
secondsTextFieldValue: TextFieldValue = TextFieldValue(),
hoursTextFieldValue: TextFieldValue = TextFieldValue("00"),
minutesTextFieldValue: TextFieldValue = TextFieldValue("00"),
secondsTextFieldValue: TextFieldValue = TextFieldValue("00"),
divider: String = stringResource(id = R.string.timer_divider),
clickable: Boolean = true,
focusManager: FocusManager
onHoursValueChanged: (TextFieldValue) -> Unit = { _ -> },
onMinutesValueChanged: (TextFieldValue) -> Unit = { _ -> },
onSecondsValueChanged: (TextFieldValue) -> Unit = { _ -> },
onHoursFocusChanged: (FocusState) -> Unit = { _ -> },
onMinutesFocusChanged: (FocusState) -> Unit = { _ -> },
onSecondsFocusChanged: (FocusState) -> Unit = { _ -> },
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current

Row {
TimerTextField(
modifier = Modifier.onFocusChanged {
viewModel.onHoursFocusChanged(it)
},
modifier = Modifier
.testTag("TimerTextField_Hours")
.onFocusChanged {
onHoursFocusChanged(it)
},
textValue = hoursTextFieldValue,
enabled = clickable,
keyboardController = keyboardController,
imeAction = ImeAction.Next,
focusManager = focusManager,
onValueChange = { viewModel.onHourValueChange(it) }
onValueChange = onHoursValueChanged
)
Text(
text = ":",
text = divider,
style = MaterialTheme.typography.h2
)
TimerTextField(
modifier = Modifier.onFocusChanged {
viewModel.onMinutesFocusChanged(it)
},
modifier = Modifier
.testTag("TimerTextField_Minutes")
.onFocusChanged {
onMinutesFocusChanged(it)
},
textValue = minutesTextFieldValue,
enabled = clickable,
keyboardController = keyboardController,
imeAction = ImeAction.Next,
focusManager = focusManager,
onValueChange = { viewModel.onMinuteValueChange(it)}
onValueChange = onMinutesValueChanged
)
Text(
text = ":",
text = divider,
style = MaterialTheme.typography.h2
)
TimerTextField(
modifier = Modifier.onFocusChanged {
viewModel.onSecondsFocusChanged(it)
},
modifier = Modifier
.testTag("TimerTextField_Seconds")
.onFocusChanged {
onSecondsFocusChanged(it)
},
textValue = secondsTextFieldValue,
enabled = clickable,
keyboardController = keyboardController,
imeAction = ImeAction.Done,
focusManager = focusManager,
onValueChange = { viewModel.onSecondValueChange(it) }
onValueChange = onSecondsValueChanged
)
}
}

@Preview
@Composable
fun EditTimerTextFieldPreview() {
//EditTimerTextField()
EditTimerTextField()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,30 @@ import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nickspatties.timeclock.R

@Composable
fun StartTimerButton(
modifier: Modifier = Modifier,
clockEnabled: Boolean,
isRunning: Boolean,
startClock: () -> Unit,
stopClock: (Boolean) -> Unit
) {
Button(
modifier = Modifier
modifier = modifier
.width(150.dp)
.height(100.dp),
shape = RoundedCornerShape(50.dp),
enabled = clockEnabled,
onClick = { if (isRunning) stopClock(true) else startClock() }
) {
Text(
text = if (isRunning) "Stop" else "Start",
text = if (isRunning) stringResource(id = R.string.stop) else stringResource(id = R.string.start),
fontSize = 24.sp
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
Expand Down Expand Up @@ -39,7 +40,10 @@ fun TaskTextField(
Text(stringResource(R.string.task_text_field_placeholder))
},
trailingIcon = {
IconButton(onClick = onIconClick) {
IconButton(
modifier = Modifier.testTag("TaskTextField_IconButton"),
onClick = onIconClick
) {
val tint = if (countdownTimerEnabled)
MaterialTheme.colors.primary
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fun TimerText(
modifier: Modifier = Modifier,
isRunning: Boolean,
currSeconds: Int,
finishedListener: () -> Unit = { }
finishedListener: () -> Unit = {}
) {
val alpha: Float by animateFloatAsState(
targetValue = if (isRunning) 1f else 0f,
Expand Down
Loading

0 comments on commit 61679ad

Please sign in to comment.