Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add back arrow to Terms of Service #3062

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion app/src/main/java/com/google/android/ground/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ class MainActivity : AbstractActivity() {
}

private fun navigateTo(directions: NavDirections) {
navHostFragment.navController.navigate(directions)
navHostFragment.navController.currentDestination?.getAction(directions.actionId)?.let { action
->
if (navHostFragment.navController.currentDestination?.id != action.destinationId) {
navHostFragment.navController.navigate(directions)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ import androidx.compose.ui.res.stringResource

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Toolbar(@StringRes stringRes: Int, iconClick: () -> Unit) {
fun Toolbar(@StringRes stringRes: Int, showNavigationIcon: Boolean = true, iconClick: () -> Unit) {
TopAppBar(
title = { Text(text = stringResource(stringRes)) },
navigationIcon = {
IconButton(onClick = iconClick) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
if (showNavigationIcon) {
IconButton(onClick = iconClick) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,37 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.ground.databinding.FragmentTermsServiceBinding
import com.google.android.ground.R
import com.google.android.ground.ui.common.AbstractFragment
import com.google.android.ground.ui.compose.HtmlText
import com.google.android.ground.ui.compose.Toolbar
import com.google.android.ground.ui.surveyselector.SurveySelectorFragmentDirections
import com.google.android.ground.util.createComposeView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

Expand All @@ -41,13 +67,60 @@
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
val args = TermsOfServiceFragmentArgs.fromBundle(requireArguments())
val binding = FragmentTermsServiceBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.isViewOnly = args.isViewOnly
binding.lifecycleOwner = this
return binding.root
): View = createComposeView {
CreateView(TermsOfServiceFragmentArgs.fromBundle(requireArguments()))
}

@Composable
private fun CreateView(args: TermsOfServiceFragmentArgs) {
Scaffold(
topBar = {
Toolbar(
stringRes = R.string.tos_title,
showNavigationIcon = args.isViewOnly,
iconClick = { findNavController().navigateUp() },
)

Check warning on line 82 in app/src/main/java/com/google/android/ground/ui/tos/TermsOfServiceFragment.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/google/android/ground/ui/tos/TermsOfServiceFragment.kt#L81-L82

Added lines #L81 - L82 were not covered by tests
}
) { innerPadding ->
val termsText by viewModel.termsOfServiceText.observeAsState(AnnotatedString(""))
val agreeChecked by viewModel.agreeCheckboxChecked.observeAsState(false)

Column(
modifier =
Modifier.padding(innerPadding)
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(16.dp))
HtmlText(
html = termsText.toString(),
modifier = Modifier.padding(8.dp).testTag("sddsfsdfsdf"),
)
Spacer(modifier = Modifier.height(16.dp))

if (!args.isViewOnly) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Checkbox(
checked = agreeChecked,
onCheckedChange = { viewModel.agreeCheckboxChecked.value = it },
)
Text(
text = stringResource(R.string.agree_checkbox),
modifier = Modifier.clickable { viewModel.agreeCheckboxChecked.value = !agreeChecked },
)
}

Spacer(modifier = Modifier.height(16.dp))

Button(onClick = { viewModel.onButtonClicked() }, enabled = agreeChecked) {
Text(text = stringResource(R.string.agree_terms))
}
Spacer(modifier = Modifier.height(32.dp))
}
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,18 @@
*/
package com.google.android.ground.ui.tos

import android.text.Html
import android.text.SpannableStringBuilder
import android.view.View
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.core.os.bundleOf
import androidx.navigation.NavController
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.google.android.ground.BaseHiltTest
import com.google.android.ground.R
import com.google.android.ground.launchFragmentInHiltContainer
Expand All @@ -42,10 +39,7 @@ import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import org.hamcrest.BaseMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.not
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
Expand All @@ -63,60 +57,63 @@ class TermsOfServiceFragmentTest : BaseHiltTest() {

@BindValue @Mock lateinit var networkManager: NetworkManager

private fun withHtml(html: String): Matcher<View> =
object : BaseMatcher<View>() {
override fun describeTo(description: Description?) {
description?.apply { this.appendText(html) }
}

override fun matches(actual: Any?): Boolean {
val textView = actual as TextView
return Html.toHtml(SpannableStringBuilder(textView.text), 0) == html
}

override fun describeMismatch(item: Any?, description: Description?) {
description?.appendText(Html.toHtml(SpannableStringBuilder((item as TextView).text), 0))
super.describeMismatch(item, description)
}
}
/**
* composeTestRule has to be created in the specific test file in order to access the required
* activity. [composeTestRule.activity]
*/
@get:Rule override val composeTestRule = createAndroidComposeRule<ComponentActivity>()

override fun setUp() {
super.setUp()
fakeRemoteDataStore.termsOfService = Result.success(TEST_TOS)
}

@Test
fun `Toolbar is displayed`() {
launchFragmentInHiltContainer<TermsOfServiceFragment>(bundleOf(Pair("isViewOnly", false)))

composeTestRule
.onNodeWithText(composeTestRule.activity.getString(R.string.tos_title))
.assertIsDisplayed()
}

@Test
fun `Toolbar Back Arrow is displayed`() {
launchFragmentInHiltContainer<TermsOfServiceFragment>(bundleOf(Pair("isViewOnly", true)))

composeTestRule.onNodeWithContentDescription("Back").assertIsDisplayed()
}

@Test
fun termsOfServiceText_shouldBeDisplayed() {
whenever(networkManager.isNetworkConnected()).thenReturn(true)
launchFragmentInHiltContainer<TermsOfServiceFragment>(bundleOf(Pair("isViewOnly", false)))

onView(withId(R.id.termsText))
.check(matches(isDisplayed()))
.check(matches(withText("This is a heading\n\nSample terms of service\n\n")))
.check(
matches(
withHtml(
"<p dir=\"ltr\"><span style=\"font-size:1.50em;\"><b>This is a heading</b></span></p>\n" +
"<p dir=\"ltr\">Sample terms of service</p>\n"
)
)
composeTestRule.onNodeWithText("This is a heading\n\nSample terms of service\n\n").isDisplayed()

composeTestRule
.onNodeWithText(
"<p dir=\"ltr\"><span style=\"font-size:1.50em;\"><b>This is a heading</b></span></p>\n" +
"<p dir=\"ltr\">Sample terms of service</p>\n"
)
.isDisplayed()
}

@Test
fun agreeButton_default_shouldNotBeEnabled() {
launchFragmentInHiltContainer<TermsOfServiceFragment>(bundleOf(Pair("isViewOnly", false)))

onView(withId(R.id.agreeCheckBox)).check(matches(isNotChecked()))
onView(withId(R.id.agreeButton)).check(matches(not(isEnabled())))
getCheckbox().assertIsDisplayed()
getButton().assertIsNotEnabled()
}

@Test
fun agreeButton_whenCheckBoxClicked_shouldBeEnabled() {
launchFragmentInHiltContainer<TermsOfServiceFragment>(bundleOf(Pair("isViewOnly", false)))

onView(withId(R.id.agreeCheckBox)).perform(click()).check(matches(isChecked()))
onView(withId(R.id.agreeButton)).check(matches(isEnabled()))
getCheckbox().performClick()

getButton().assertIsEnabled()
}

@Test
Expand All @@ -129,22 +126,27 @@ class TermsOfServiceFragmentTest : BaseHiltTest() {

assertThat(termsOfServiceRepository.isTermsOfServiceAccepted).isFalse()

onView(withId(R.id.agreeCheckBox)).perform(click())
onView(withId(R.id.agreeButton)).perform(click())
getCheckbox().performClick()
getButton().performClick()

assertThat(navController.currentDestination?.id).isEqualTo(R.id.surveySelectorFragment)

assertThat(termsOfServiceRepository.isTermsOfServiceAccepted).isTrue()
}

@Test
fun viewOnlyMode_controlsHidden() = runWithTestDispatcher {
launchFragmentInHiltContainer<TermsOfServiceFragment>(bundleOf(Pair("isViewOnly", true)))

onView(withId(R.id.agreeCheckBox)).check(matches(not(isDisplayed())))
onView(withId(R.id.agreeButton)).check(matches(not(isDisplayed())))
getCheckbox().assertIsNotDisplayed()
getButton().assertIsNotDisplayed()
}

private fun getCheckbox() =
composeTestRule.onNodeWithText(composeTestRule.activity.getString(R.string.agree_checkbox))

private fun getButton() =
composeTestRule.onNodeWithText(composeTestRule.activity.getString(R.string.agree_terms))

companion object {
const val TEST_TOS_TEXT = "# This is a heading\n\nSample terms of service"
val TEST_TOS = TermsOfService("TERMS_OF_SERVICE", TEST_TOS_TEXT)
Expand Down