Use vvalidator in the view site page

This commit is contained in:
Aidan Follestad 2019-01-26 14:10:58 -08:00
parent b3f8a43f71
commit c6fca52fe4
4 changed files with 47 additions and 377 deletions

View file

@ -72,9 +72,9 @@ class AddSiteActivity : DarkModeSwitchActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_addsite)
setupUi()
setupValidation()
lifecycle.addObserver(viewModel)
// Populate view model with initial data

View file

@ -32,11 +32,13 @@ import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
import com.afollestad.nocknock.utilities.livedata.distinct
import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
import com.afollestad.nocknock.viewcomponents.ext.onScroll
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
import com.afollestad.vvalidator.form
import com.afollestad.vvalidator.form.Form
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
import kotlinx.android.synthetic.main.activity_viewsite.headersLayout
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
@ -69,6 +71,7 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
}
internal val viewModel by viewModel<ViewSiteViewModel>()
private lateinit var validationForm: Form
private val intentProvider by inject<IntentProvider>()
private val statusUpdateReceiver by lazy {
@ -82,6 +85,7 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewsite)
setupUi()
setupValidation()
lifecycle.run {
addObserver(viewModel)
@ -103,23 +107,17 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
// Name
inputName.attachLiveData(this, viewModel.name)
viewModel.onNameError()
.toViewError(this, inputName)
// Tags
inputTags.attachLiveData(this, viewModel.tags)
// Url
inputUrl.attachLiveData(this, viewModel.url)
viewModel.onUrlError()
.toViewError(this, inputUrl)
viewModel.onUrlWarningVisibility()
.toViewVisibility(this, textUrlWarning)
// Timeout
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
viewModel.onTimeoutError()
.toViewError(this, responseTimeoutInput)
// Validation mode
responseValidationMode.attachLiveData(
@ -128,8 +126,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
outTransformer = { ValidationMode.fromIndex(it) },
inTransformer = { it.toIndex() }
)
viewModel.onValidationSearchTermError()
.toViewError(this, responseValidationSearchTerm)
viewModel.onValidationModeDescription()
.toViewText(this, validationModeDescription)
@ -141,15 +137,15 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
// Validation script
scriptInputLayout.attach(
codeData = viewModel.validationScript,
errorData = viewModel.onValidationScriptError(),
visibility = viewModel.onValidationScriptVisibility()
visibility = viewModel.onValidationScriptVisibility(),
form = validationForm
)
// Check interval
checkIntervalLayout.attach(
valueData = viewModel.checkIntervalValue,
multiplierData = viewModel.checkIntervalUnit,
errorData = viewModel.onCheckIntervalError()
form = validationForm
)
// Retry Policy
@ -162,8 +158,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
viewModel.certificateUri.distinct()
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
viewModel.onCertificateError()
.toViewError(this, sslCertificateInput)
// Headers
headersLayout.attach(viewModel.headers)
@ -190,7 +184,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.commit -> viewModel.commit { finish() }
R.id.remove -> maybeRemoveSite()
R.id.disableChecks -> maybeDisableChecks()
}
@ -238,6 +231,35 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
}
}
private fun setupValidation() {
validationForm = form {
input(inputName, name = "Name") {
isNotEmpty().description(R.string.please_enter_name)
}
input(inputUrl, name = "URL") {
isNotEmpty().description(R.string.please_enter_url)
isUrl().description(R.string.please_enter_valid_url)
}
input(responseTimeoutInput, name = "Timeout", optional = true) {
isNumber().greaterThan(0)
.description(R.string.please_enter_networkTimeout)
}
input(responseValidationSearchTerm, name = "Search term") {
conditional(responseValidationSearchTerm.isVisibleCondition()) {
isNotEmpty().description(R.string.please_enter_search_term)
}
}
input(sslCertificateInput, name = "Certificate Path") {
isUri().hasScheme("file", "content")
.that { it.host != null }
.description(R.string.please_enter_validCertUri)
}
submitWith(toolbar.menu, R.id.commit) {
viewModel.commit { finish() }
}
}
}
override fun onResume() {
super.onResume()
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {

View file

@ -40,11 +40,9 @@ import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.ScopedViewModel
import com.afollestad.nocknock.utilities.ext.formatDate
import com.afollestad.nocknock.utilities.ext.toUri
import com.afollestad.nocknock.utilities.livedata.map
import com.afollestad.nocknock.utilities.livedata.zip
import com.afollestad.nocknock.utilities.providers.StringProvider
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -81,23 +79,10 @@ class ViewSiteViewModel(
internal val disabled = MutableLiveData<Boolean>()
internal val lastResult = MutableLiveData<ValidationResult?>()
// Private properties
private val isLoading = MutableLiveData<Boolean>()
private val nameError = MutableLiveData<Int?>()
private val urlError = MutableLiveData<Int?>()
private val timeoutError = MutableLiveData<Int?>()
private val validationSearchTermError = MutableLiveData<Int?>()
private val validationScriptError = MutableLiveData<Int?>()
private val checkIntervalValueError = MutableLiveData<Int?>()
private val certificateError = MutableLiveData<Int?>()
// Expose private properties or calculated properties
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
@CheckResult fun onNameError(): LiveData<Int?> = nameError
@CheckResult fun onUrlError(): LiveData<Int?> = urlError
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
return url.map {
val parsed = HttpUrl.parse(it)
@ -105,8 +90,6 @@ class ViewSiteViewModel(
}
}
@CheckResult fun onTimeoutError(): LiveData<Int?> = timeoutError
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
return validationMode.map {
when (it!!) {
@ -117,22 +100,11 @@ class ViewSiteViewModel(
}
}
@CheckResult fun onValidationSearchTermError(): LiveData<Int?> = validationSearchTermError
@CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
@CheckResult fun onValidationSearchTermVisibility() =
validationMode.map { it == TERM_SEARCH }
@CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
@CheckResult fun onValidationScriptError(): LiveData<Int?> = validationScriptError
@CheckResult fun onValidationScriptVisibility() =
validationMode.map { it == JAVASCRIPT }
@CheckResult fun onCheckIntervalError(): LiveData<Int?> = checkIntervalValueError
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> =
disabled.map { !it }
@CheckResult fun onCertificateError(): LiveData<Int?> = certificateError
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> = disabled.map { !it }
@CheckResult fun onDoneButtonText(): LiveData<Int> =
disabled.map {
@ -250,89 +222,7 @@ class ViewSiteViewModel(
}
private fun getUpdatedDbModel(): Site? {
var errorCount = 0
// Validation name
if (name.value.isNullOrEmpty()) {
nameError.value = R.string.please_enter_name
errorCount++
} else {
nameError.value = null
}
// Validate URL
when {
url.value.isNullOrEmpty() -> {
urlError.value = R.string.please_enter_url
errorCount++
}
HttpUrl.parse(url.value!!) == null -> {
urlError.value = R.string.please_enter_valid_url
errorCount++
}
else -> {
urlError.value = null
}
}
// Validate timeout
val timeout = timeout.value ?: 10_000
if (timeout < 0) {
timeoutError.value = R.string.please_enter_networkTimeout
errorCount++
} else {
timeoutError.value = null
}
// Validate check interval
if (checkIntervalValue.value.isNullOrLessThan(1)) {
checkIntervalValueError.value = R.string.please_enter_check_interval
errorCount++
} else {
checkIntervalValueError.value = null
}
// Validate arguments
if (validationMode.value == TERM_SEARCH &&
validationSearchTerm.value.isNullOrEmpty()
) {
errorCount++
validationSearchTermError.value = R.string.please_enter_search_term
validationScriptError.value = null
} else if (validationMode.value == JAVASCRIPT &&
validationScript.value.isNullOrEmpty()
) {
errorCount++
validationSearchTermError.value = null
validationScriptError.value = R.string.please_enter_javaScript
} else {
validationSearchTermError.value = null
validationScriptError.value = null
}
// Validate SSL certificate
val certString = certificateUri.value
if (certString != null) {
val rawCertUri = certString.toUri()
val certUri = if (rawCertUri.scheme == null) {
rawCertUri.buildUpon()
.scheme("file")
.build()
} else {
rawCertUri
}
if (certUri.scheme != "content" && certUri.scheme != "file") {
errorCount++
certificateError.value = R.string.please_enter_validCertUri
} else {
certificateError.value = null
}
}
if (errorCount > 0) {
return null
}
val cleanedTags = tags.value?.split(',')?.joinToString { it.trim() } ?: ""
val newSettings = site.settings!!.copy(
@ -349,11 +239,15 @@ class ViewSiteViewModel(
val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
if (site.retryPolicy != null) {
// Have existing policy, update it
site.retryPolicy!!.copy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
site.retryPolicy!!.copy(
count = retryPolicyTimes,
minutes = retryPolicyMinutes
)
} else {
// Create new policy
RetryPolicy(
count = retryPolicyTimes, minutes = retryPolicyMinutes
count = retryPolicyTimes,
minutes = retryPolicyMinutes
)
}
} else {

View file

@ -39,7 +39,6 @@ import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.doAnswer
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -255,247 +254,9 @@ class ViewSiteViewModelTest {
.isEqualTo("Two")
}
@Test fun commit_nameError() {
val onNameError = viewModel.onNameError()
.test()
val onUrlError = viewModel.onUrlError()
.test()
val onTimeoutError = viewModel.onTimeoutError()
.test()
val onCheckIntervalError = viewModel.onCheckIntervalError()
.test()
val onSearchTermError = viewModel.onValidationSearchTermError()
.test()
val onScriptError = viewModel.onValidationScriptError()
.test()
fillInModel().apply {
name.value = ""
}
val onDone = mock<() -> Unit>()
viewModel.commit(onDone)
verify(validationManager, never())
.scheduleValidation(any(), any(), any(), any())
onNameError.assertValues(R.string.please_enter_name)
onUrlError.assertNoValues()
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertNoValues()
verify(onDone, never()).invoke()
}
@Test fun commit_urlEmptyError() {
val onNameError = viewModel.onNameError()
.test()
val onUrlError = viewModel.onUrlError()
.test()
val onTimeoutError = viewModel.onTimeoutError()
.test()
val onCheckIntervalError = viewModel.onCheckIntervalError()
.test()
val onSearchTermError = viewModel.onValidationSearchTermError()
.test()
val onScriptError = viewModel.onValidationScriptError()
.test()
fillInModel().apply {
url.value = ""
}
val onDone = mock<() -> Unit>()
viewModel.commit(onDone)
verify(validationManager, never())
.scheduleValidation(any(), any(), any(), any())
onNameError.assertNoValues()
onUrlError.assertValues(R.string.please_enter_url)
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertNoValues()
verify(onDone, never()).invoke()
}
@Test fun commit_urlFormatError() {
val onNameError = viewModel.onNameError()
.test()
val onUrlError = viewModel.onUrlError()
.test()
val onTimeoutError = viewModel.onTimeoutError()
.test()
val onCheckIntervalError = viewModel.onCheckIntervalError()
.test()
val onSearchTermError = viewModel.onValidationSearchTermError()
.test()
val onScriptError = viewModel.onValidationScriptError()
.test()
fillInModel().apply {
url.value = "ftp://www.idk.com"
}
val onDone = mock<() -> Unit>()
viewModel.commit(onDone)
verify(validationManager, never())
.scheduleValidation(any(), any(), any(), any())
onNameError.assertNoValues()
onUrlError.assertValues(R.string.please_enter_valid_url)
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertNoValues()
verify(onDone, never()).invoke()
}
@Test fun commit_networkTimeout_error() {
val onNameError = viewModel.onNameError()
.test()
val onUrlError = viewModel.onUrlError()
.test()
val onTimeoutError = viewModel.onTimeoutError()
.test()
val onCheckIntervalError = viewModel.onCheckIntervalError()
.test()
val onSearchTermError = viewModel.onValidationSearchTermError()
.test()
val onScriptError = viewModel.onValidationScriptError()
.test()
fillInModel().apply {
timeout.value = 0
}
val onDone = mock<() -> Unit>()
viewModel.commit(onDone)
verify(validationManager, never())
.scheduleValidation(any(), any(), any(), any())
onNameError.assertNoValues()
onUrlError.assertNoValues()
onTimeoutError.assertValues(R.string.please_enter_networkTimeout)
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertNoValues()
verify(onDone, never()).invoke()
}
@Test fun commit_checkIntervalError() {
val onNameError = viewModel.onNameError()
.test()
val onUrlError = viewModel.onUrlError()
.test()
val onTimeoutError = viewModel.onTimeoutError()
.test()
val onCheckIntervalError = viewModel.onCheckIntervalError()
.test()
val onSearchTermError = viewModel.onValidationSearchTermError()
.test()
val onScriptError = viewModel.onValidationScriptError()
.test()
fillInModel().apply {
checkIntervalValue.value = 0
}
val onDone = mock<() -> Unit>()
viewModel.commit(onDone)
verify(validationManager, never())
.scheduleValidation(any(), any(), any(), any())
onNameError.assertNoValues()
onUrlError.assertNoValues()
onTimeoutError.assertNoValues()
onCheckIntervalError.assertValues(R.string.please_enter_check_interval)
onSearchTermError.assertNoValues()
onScriptError.assertNoValues()
verify(onDone, never()).invoke()
}
@Test fun commit_termSearchError() {
val onNameError = viewModel.onNameError()
.test()
val onUrlError = viewModel.onUrlError()
.test()
val onTimeoutError = viewModel.onTimeoutError()
.test()
val onCheckIntervalError = viewModel.onCheckIntervalError()
.test()
val onSearchTermError = viewModel.onValidationSearchTermError()
.test()
val onScriptError = viewModel.onValidationScriptError()
.test()
fillInModel().apply {
validationMode.value = TERM_SEARCH
validationSearchTerm.value = ""
}
val onDone = mock<() -> Unit>()
viewModel.commit(onDone)
verify(validationManager, never())
.scheduleValidation(any(), any(), any(), any())
onNameError.assertNoValues()
onUrlError.assertNoValues()
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertValues(R.string.please_enter_search_term)
onScriptError.assertNoValues()
verify(onDone, never()).invoke()
}
@Test fun commit_javaScript_error() {
val onNameError = viewModel.onNameError()
.test()
val onUrlError = viewModel.onUrlError()
.test()
val onTimeoutError = viewModel.onTimeoutError()
.test()
val onCheckIntervalError = viewModel.onCheckIntervalError()
.test()
val onSearchTermError = viewModel.onValidationSearchTermError()
.test()
val onScriptError = viewModel.onValidationScriptError()
.test()
fillInModel().apply {
validationMode.value = JAVASCRIPT
validationScript.value = ""
}
val onDone = mock<() -> Unit>()
viewModel.commit(onDone)
verify(validationManager, never())
.scheduleValidation(any(), any(), any(), any())
onNameError.assertNoValues()
onUrlError.assertNoValues()
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertValues(R.string.please_enter_javaScript)
verify(onDone, never()).invoke()
}
@Test fun commit_success() = runBlocking {
val isLoading = viewModel.onIsLoading()
.test()
val onNameError = viewModel.onNameError()
.test()
val onUrlError = viewModel.onUrlError()
.test()
val onTimeoutError = viewModel.onTimeoutError()
.test()
val onSearchTermError = viewModel.onValidationSearchTermError()
.test()
val onScriptError = viewModel.onValidationScriptError()
.test()
val onCheckIntervalError = viewModel.onCheckIntervalError()
.test()
fillInModel()
val onDone = mock<() -> Unit>()
@ -541,13 +302,6 @@ class ViewSiteViewModelTest {
fromFinishingJob = false
)
onNameError.assertNoValues()
onUrlError.assertNoValues()
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertNoValues()
verify(onDone).invoke()
}