diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.java deleted file mode 100644 index 45ea10e9779..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2009 University of Washington - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package org.odk.collect.android.widgets; - -import static org.odk.collect.android.utilities.Appearances.FRONT; -import static org.odk.collect.android.utilities.Appearances.hasAppearance; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.util.TypedValue; -import android.view.View; - -import com.google.zxing.integration.android.IntentIntegrator; - -import org.javarosa.core.model.data.IAnswerData; -import org.javarosa.core.model.data.StringData; -import org.javarosa.form.api.FormEntryPrompt; -import org.odk.collect.android.activities.ScannerWithFlashlightActivity; -import org.odk.collect.android.databinding.BarcodeWidgetAnswerBinding; -import org.odk.collect.android.formentry.questions.QuestionDetails; -import org.odk.collect.android.utilities.Appearances; -import org.odk.collect.androidshared.system.CameraUtils; -import org.odk.collect.androidshared.ui.ToastUtils; -import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver; -import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry; - -/** - * Widget that allows user to scan barcodes and add them to the form. - */ - -@SuppressLint("ViewConstructor") -public class BarcodeWidget extends QuestionWidget implements WidgetDataReceiver { - BarcodeWidgetAnswerBinding binding; - - private final WaitingForDataRegistry waitingForDataRegistry; - private final CameraUtils cameraUtils; - - public BarcodeWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry, - CameraUtils cameraUtils) { - super(context, questionDetails); - render(); - - this.waitingForDataRegistry = waitingForDataRegistry; - this.cameraUtils = cameraUtils; - } - - @Override - protected View onCreateAnswerView(Context context, FormEntryPrompt prompt, int answerFontSize) { - binding = BarcodeWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater()); - - if (prompt.isReadOnly()) { - binding.barcodeButton.setVisibility(GONE); - } else { - binding.barcodeButton.setOnClickListener(v -> onButtonClick()); - } - binding.barcodeAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize); - - String answer = prompt.getAnswerText(); - if (answer != null && !answer.isEmpty()) { - binding.barcodeButton.setText(getContext().getString(org.odk.collect.strings.R.string.replace_barcode)); - binding.barcodeAnswerText.setText(answer); - } - - updateVisibility(); - return binding.getRoot(); - } - - @Override - public void clearAnswer() { - binding.barcodeAnswerText.setText(null); - binding.barcodeButton.setText(getContext().getString(org.odk.collect.strings.R.string.get_barcode)); - widgetValueChanged(); - - updateVisibility(); - } - - @Override - public IAnswerData getAnswer() { - String answer = binding.barcodeAnswerText.getText().toString(); - return answer.isEmpty() ? null : new StringData(answer); - } - - @Override - public void setData(Object answer) { - String response = (String) answer; - - binding.barcodeAnswerText.setText(stripInvalidCharacters(response)); - binding.barcodeButton.setText(getContext().getString(org.odk.collect.strings.R.string.replace_barcode)); - updateVisibility(); - - widgetValueChanged(); - } - - private void updateVisibility() { - if (hasAppearance(getFormEntryPrompt(), Appearances.HIDDEN_ANSWER)) { - binding.barcodeAnswerText.setVisibility(GONE); - } else { - binding.barcodeAnswerText.setVisibility(binding.barcodeAnswerText.getText().toString().isBlank() ? GONE : VISIBLE); - } - } - - // Remove control characters, invisible characters and unused code points. - private String stripInvalidCharacters(String data) { - return data == null ? null : data.replaceAll("\\p{C}", ""); - } - - @Override - public void setOnLongClickListener(OnLongClickListener l) { - binding.barcodeAnswerText.setOnLongClickListener(l); - binding.barcodeButton.setOnLongClickListener(l); - } - - @Override - public void cancelLongPress() { - super.cancelLongPress(); - binding.barcodeButton.cancelLongPress(); - binding.barcodeAnswerText.cancelLongPress(); - } - - private void onButtonClick() { - getPermissionsProvider().requestCameraPermission((Activity) getContext(), () -> { - waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex()); - - IntentIntegrator intent = new IntentIntegrator((Activity) getContext()) - .setCaptureActivity(ScannerWithFlashlightActivity.class); - - setCameraIdIfNeeded(getFormEntryPrompt(), intent); - intent.initiateScan(); - }); - } - - private void setCameraIdIfNeeded(FormEntryPrompt prompt, IntentIntegrator intent) { - if (Appearances.isFrontCameraAppearance(prompt)) { - if (cameraUtils.isFrontCameraAvailable(getContext())) { - intent.addExtra(FRONT, true); - } else { - ToastUtils.showLongToast(getContext(), org.odk.collect.strings.R.string.error_front_camera_unavailable); - } - } - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.kt new file mode 100644 index 00000000000..b9f69667655 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.kt @@ -0,0 +1,109 @@ +package org.odk.collect.android.widgets + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.view.View +import com.google.zxing.integration.android.IntentIntegrator +import org.javarosa.core.model.data.IAnswerData +import org.javarosa.core.model.data.StringData +import org.javarosa.form.api.FormEntryPrompt +import org.odk.collect.android.activities.ScannerWithFlashlightActivity +import org.odk.collect.android.databinding.BarcodeWidgetBinding +import org.odk.collect.android.formentry.questions.QuestionDetails +import org.odk.collect.android.utilities.Appearances +import org.odk.collect.android.utilities.Appearances.hasAppearance +import org.odk.collect.android.utilities.Appearances.isFrontCameraAppearance +import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver +import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry +import org.odk.collect.androidshared.system.CameraUtils +import org.odk.collect.androidshared.ui.ToastUtils.showLongToast +import org.odk.collect.permissions.PermissionListener +import org.odk.collect.strings.R + +@SuppressLint("ViewConstructor") +class BarcodeWidget( + context: Context, + questionDetails: QuestionDetails, + private val waitingForDataRegistry: WaitingForDataRegistry, + private val cameraUtils: CameraUtils +) : QuestionWidget(context, questionDetails), WidgetDataReceiver { + lateinit var binding: BarcodeWidgetBinding + + init { + render() + } + + override fun onCreateAnswerView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View { + binding = BarcodeWidgetBinding.inflate((context as Activity).layoutInflater) + + if (prompt.isReadOnly) { + binding.barcodeButton.visibility = GONE + } else { + binding.barcodeButton.setOnClickListener { onButtonClick() } + } + binding.answerView.setHidden(hasAppearance(prompt, Appearances.HIDDEN_ANSWER)) + binding.answerView.setTextSize(answerFontSize.toFloat()) + + val answer = prompt.answerText + if (!answer.isNullOrEmpty()) { + binding.barcodeButton.text = getContext().getString(R.string.replace_barcode) + } + binding.answerView.setAnswer(prompt.answerText) + + return binding.root + } + + override fun clearAnswer() { + binding.answerView.setAnswer(null) + binding.barcodeButton.text = context.getString(R.string.get_barcode) + widgetValueChanged() + } + + override fun getAnswer(): IAnswerData? { + val answer = binding.answerView.getAnswer() + return if (answer.isEmpty()) null else StringData(answer) + } + + override fun setData(answer: Any) { + binding.answerView.setAnswer(answer as String) + binding.barcodeButton.text = context.getString(R.string.replace_barcode) + widgetValueChanged() + } + + override fun setOnLongClickListener(l: OnLongClickListener?) { + binding.barcodeButton.setOnLongClickListener(l) + binding.answerView.setOnLongClickListener(l) + } + + override fun cancelLongPress() { + super.cancelLongPress() + binding.barcodeButton.cancelLongPress() + binding.answerView.cancelLongPress() + } + + private fun onButtonClick() { + getPermissionsProvider().requestCameraPermission( + (context as Activity), + object : PermissionListener { + override fun granted() { + waitingForDataRegistry.waitForData(formEntryPrompt.index) + val intent = IntentIntegrator(context as Activity) + .setCaptureActivity(ScannerWithFlashlightActivity::class.java) + setCameraIdIfNeeded(formEntryPrompt, intent) + intent.initiateScan() + } + } + ) + } + + private fun setCameraIdIfNeeded(prompt: FormEntryPrompt, intent: IntentIntegrator) { + if (isFrontCameraAppearance(prompt)) { + if (cameraUtils.isFrontCameraAvailable(context)) { + intent.addExtra(Appearances.FRONT, true) + } else { + showLongToast(context, R.string.error_front_camera_unavailable) + } + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidgetAnswer.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidgetAnswer.kt new file mode 100644 index 00000000000..18f5f10d443 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidgetAnswer.kt @@ -0,0 +1,39 @@ +package org.odk.collect.android.widgets + +import android.content.Context +import android.util.AttributeSet +import android.util.TypedValue +import android.view.LayoutInflater +import android.widget.FrameLayout +import org.odk.collect.android.databinding.BarcodeWidgetAnswerBinding + +class BarcodeWidgetAnswer @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : FrameLayout(context, attrs, defStyle) { + private val binding = BarcodeWidgetAnswerBinding.inflate(LayoutInflater.from(context), this, true) + private var hidden = false + + fun setAnswer(answer: String?) { + binding.answer.text = stripInvalidCharacters(answer) + binding.root.visibility = if (hidden || binding.answer.text.isNullOrBlank()) GONE else VISIBLE + } + + fun setTextSize(textSize: Float) { + binding.answer.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) + } + + fun setHidden(hidden: Boolean) { + this.hidden = hidden + } + + fun getAnswer(): String { + return binding.answer.text.toString() + } + + // Remove control characters, invisible characters and unused code points. + private fun stripInvalidCharacters(data: String?): String? { + return data?.replace("\\p{C}".toRegex(), "") + } +} diff --git a/collect_app/src/main/res/layout/barcode_widget.xml b/collect_app/src/main/res/layout/barcode_widget.xml new file mode 100644 index 00000000000..913a568ad0d --- /dev/null +++ b/collect_app/src/main/res/layout/barcode_widget.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/collect_app/src/main/res/layout/barcode_widget_answer.xml b/collect_app/src/main/res/layout/barcode_widget_answer.xml index 1fc1bcf1a19..daf129fee74 100644 --- a/collect_app/src/main/res/layout/barcode_widget_answer.xml +++ b/collect_app/src/main/res/layout/barcode_widget_answer.xml @@ -1,20 +1,28 @@ - - + app:tint="?colorOnSurface" + app:srcCompat="@drawable/ic_baseline_barcode_scanner_white_24" + app:layout_constraintTop_toTopOf="@id/answer" + app:layout_constraintBottom_toBottomOf="@id/answer" + app:layout_constraintStart_toStartOf="parent" /> + android:id="@+id/answer" + style="@style/Widget.Collect.TextView.WidgetAnswer" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/icon" + tools:text="123456789" /> - + \ No newline at end of file diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.java deleted file mode 100644 index 24867ce6d46..00000000000 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.java +++ /dev/null @@ -1,211 +0,0 @@ -package org.odk.collect.android.widgets; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener; -import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer; -import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAppearance; -import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly; -import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity; -import static org.robolectric.Shadows.shadowOf; - -import android.view.View; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.javarosa.core.model.FormIndex; -import org.javarosa.core.model.data.StringData; -import org.javarosa.form.api.FormEntryPrompt; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.odk.collect.android.fakes.FakePermissionsProvider; -import org.odk.collect.android.formentry.questions.QuestionDetails; -import org.odk.collect.android.listeners.WidgetValueChangedListener; -import org.odk.collect.android.support.MockFormEntryPromptBuilder; -import org.odk.collect.android.support.WidgetTestActivity; -import org.odk.collect.android.utilities.Appearances; -import org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry; -import org.odk.collect.androidshared.system.CameraUtils; -import org.robolectric.shadows.ShadowActivity; -import org.robolectric.shadows.ShadowToast; - -/** - * @author James Knight - */ -@RunWith(AndroidJUnit4.class) -public class BarcodeWidgetTest { - private final FakeWaitingForDataRegistry waitingForDataRegistry = new FakeWaitingForDataRegistry(); - private final FakePermissionsProvider permissionsProvider = new FakePermissionsProvider(); - - private WidgetTestActivity widgetTestActivity; - private ShadowActivity shadowActivity; - private CameraUtils cameraUtils; - private View.OnLongClickListener listener; - private FormIndex formIndex; - - @Before - public void setUp() { - widgetTestActivity = widgetTestActivity(); - shadowActivity = shadowOf(widgetTestActivity); - - cameraUtils = mock(CameraUtils.class); - listener = mock(View.OnLongClickListener.class); - formIndex = mock(FormIndex.class); - permissionsProvider.setPermissionGranted(true); - } - - @Test - public void usingReaDOnly_shouldHideBarcodeButton() { - assertThat(createWidget(promptWithReadOnly()).binding.barcodeButton.getVisibility(), equalTo(View.GONE)); - } - - @Test - public void whenPromptHasAnswer_replaceBarcodeButtonIsDisplayed() { - BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah"))); - assertThat(widget.binding.barcodeButton.getText().toString(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.replace_barcode))); - } - - @Test - public void whenPromptHasAnswer_answerTextViewShowsCorrectAnswer() { - BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah"))); - assertThat(widget.binding.barcodeAnswerText.getText().toString(), equalTo("blah")); - } - - @Test - public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() { - BarcodeWidget widget = createWidget(promptWithAnswer(null)); - assertThat(widget.getAnswer(), nullValue()); - } - - @Test - public void getAnswer_whenPromptHasAnswer_returnsCorrectAnswer() { - BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah"))); - assertThat(widget.getAnswer().getDisplayText(), equalTo("blah")); - } - - @Test - public void clearAnswer_clearsWidgetAnswer() { - BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah"))); - widget.clearAnswer(); - - assertThat(widget.binding.barcodeAnswerText.getText().toString(), equalTo("")); - assertThat(widget.binding.barcodeButton.getText().toString(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.get_barcode))); - } - - @Test - public void clearAnswer_callsValueChangeListener() { - BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah"))); - WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget); - widget.clearAnswer(); - - verify(valueChangedListener).widgetValueChanged(widget); - } - - @Test - public void setData_updatesWidgetAnswer_afterStrippingInvalidCharacters() { - BarcodeWidget widget = createWidget(promptWithAnswer(null)); - widget.setData("\ud800blah\b"); - assertThat(widget.binding.barcodeAnswerText.getText().toString(), equalTo("blah")); - } - - @Test - public void setData_updatesButtonLabel() { - BarcodeWidget widget = createWidget(promptWithAnswer(null)); - widget.setData("\ud800blah\b"); - assertThat(widget.binding.barcodeButton.getText(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.replace_barcode))); - } - - @Test - public void setData_callsValueChangeListener() { - BarcodeWidget widget = createWidget(promptWithAnswer(null)); - WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget); - widget.setData("blah"); - - verify(valueChangedListener).widgetValueChanged(widget); - } - - @Test - public void clickingButtonAndAnswerTextViewForLong_callsLongClickListener() { - BarcodeWidget widget = createWidget(promptWithAnswer(null)); - widget.setOnLongClickListener(listener); - widget.binding.barcodeButton.performLongClick(); - widget.binding.barcodeAnswerText.performLongClick(); - - verify(listener).onLongClick(widget.binding.barcodeButton); - verify(listener).onLongClick(widget.binding.barcodeAnswerText); - } - - @Test - public void clickingBarcodeButton_whenPermissionIsNotGranted_doesNotLaunchAnyIntent() { - BarcodeWidget widget = createWidget(promptWithAnswer(null)); - permissionsProvider.setPermissionGranted(false); - widget.setPermissionsProvider(permissionsProvider); - widget.binding.barcodeButton.performClick(); - - assertThat(shadowActivity.getNextStartedActivity(), nullValue()); - assertThat(waitingForDataRegistry.waiting.isEmpty(), equalTo(true)); - } - - @Test - public void clickingBarcodeButton_whenPermissionIsGranted_setsWidgetWaitingForData() { - FormEntryPrompt prompt = promptWithAnswer(null); - when(prompt.getIndex()).thenReturn(formIndex); - - BarcodeWidget widget = createWidget(prompt); - widget.setPermissionsProvider(permissionsProvider); - widget.binding.barcodeButton.performClick(); - - assertThat(waitingForDataRegistry.waiting.contains(formIndex), equalTo(true)); - } - - @Test - public void clickingBarcodeButton_whenFrontCameraIsNotAvailable_showsFrontCameraNotAvailableToast() { - when(cameraUtils.isFrontCameraAvailable(any())).thenReturn(false); - BarcodeWidget widget = createWidget(promptWithAppearance(Appearances.FRONT)); - widget.setPermissionsProvider(permissionsProvider); - widget.binding.barcodeButton.performClick(); - - assertThat(ShadowToast.getTextOfLatestToast(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.error_front_camera_unavailable))); - } - - @Test - public void clickingBarcodeButton_whenFrontCameraIsAvailable_launchesCorrectIntent() { - when(cameraUtils.isFrontCameraAvailable(any())).thenReturn(true); - BarcodeWidget widget = createWidget(promptWithAppearance(Appearances.FRONT)); - widget.setPermissionsProvider(permissionsProvider); - widget.binding.barcodeButton.performClick(); - - assertThat(shadowActivity.getNextStartedActivity().getBooleanExtra(Appearances.FRONT, false), equalTo(true)); - } - - @Test - public void whenPromptHasHiddenAnswerAppearance_answerIsNotDisplayed() { - FormEntryPrompt prompt = new MockFormEntryPromptBuilder(promptWithAppearance(Appearances.HIDDEN_ANSWER)) - .withAnswer(new StringData("original contents")) - .build(); - - BarcodeWidget widget = createWidget(prompt); - - // Check initial value is not shown - assertThat(widget.binding.barcodeAnswerText.getVisibility(), equalTo(View.GONE)); - assertThat(widget.binding.barcodeButton.getText(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.replace_barcode))); - assertThat(widget.getAnswer(), equalTo(new StringData("original contents"))); - - // Check updates aren't shown - widget.setData("updated contents"); - assertThat(widget.binding.barcodeAnswerText.getVisibility(), equalTo(View.GONE)); - assertThat(widget.binding.barcodeButton.getText(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.replace_barcode))); - assertThat(widget.getAnswer(), equalTo(new StringData("updated contents"))); - } - - public BarcodeWidget createWidget(FormEntryPrompt prompt) { - return new BarcodeWidget(widgetTestActivity, new QuestionDetails(prompt), - waitingForDataRegistry, cameraUtils); - } -} diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.kt new file mode 100644 index 00000000000..050ea48325f --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.kt @@ -0,0 +1,241 @@ +package org.odk.collect.android.widgets + +import android.view.View +import android.view.View.OnLongClickListener +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.javarosa.core.model.FormIndex +import org.javarosa.core.model.data.StringData +import org.javarosa.form.api.FormEntryPrompt +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.verify +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.odk.collect.android.R +import org.odk.collect.android.fakes.FakePermissionsProvider +import org.odk.collect.android.formentry.questions.QuestionDetails +import org.odk.collect.android.support.MockFormEntryPromptBuilder +import org.odk.collect.android.utilities.Appearances +import org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry +import org.odk.collect.android.widgets.support.QuestionWidgetHelpers +import org.odk.collect.androidshared.system.CameraUtils +import org.odk.collect.strings.R.string +import org.robolectric.Shadows +import org.robolectric.shadows.ShadowToast + +@RunWith(AndroidJUnit4::class) +class BarcodeWidgetTest { + private val waitingForDataRegistry = FakeWaitingForDataRegistry() + private val permissionsProvider = FakePermissionsProvider().apply { + setPermissionGranted(true) + } + private val widgetTestActivity = QuestionWidgetHelpers.widgetTestActivity() + private val shadowActivity = Shadows.shadowOf(widgetTestActivity) + private val cameraUtils = mock() + private val listener = mock() + private val formIndex = mock() + + @Test + fun `The button is hidden in read-only mode`() { + assertThat( + createWidget(QuestionWidgetHelpers.promptWithReadOnly()).binding.barcodeButton.visibility, + equalTo(View.GONE) + ) + } + + @Test + fun `Display the 'Replace Barcode' button if answer is present`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah"))) + assertThat( + widget.binding.barcodeButton.text.toString(), + equalTo(widgetTestActivity.getString(string.replace_barcode)) + ) + } + + @Test + fun `Display the answer if answer is present`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah"))) + assertThat( + widget.binding.answerView.getAnswer(), + equalTo("blah") + ) + } + + @Test + fun `#getAnswer returns null when there is no answer`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + assertThat(widget.answer, equalTo(null)) + } + + @Test + fun `#getAnswer returns the answer when there is answer`() { + val widget = createWidget( + QuestionWidgetHelpers.promptWithAnswer( + StringData("blah") + ) + ) + assertThat(widget.answer!!.displayText, equalTo("blah")) + } + + @Test + fun `#clearAnswer removes answer and updates button title`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah"))) + widget.clearAnswer() + + assertThat( + widget.binding.answerView.getAnswer(), + equalTo("") + ) + assertThat( + widget.binding.barcodeButton.text.toString(), + equalTo(widgetTestActivity.getString(string.get_barcode)) + ) + } + + @Test + fun `#clearAnswer calls #valueChangeListener`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah"))) + val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener(widget) + widget.clearAnswer() + + verify(valueChangedListener).widgetValueChanged(widget) + } + + @Test + fun `#setData displays sanitized answer`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + widget.setData("\ud800blah\b") + assertThat( + widget.binding.answerView.getAnswer(), + equalTo("blah") + ) + } + + @Test + fun `#setData updates the button title`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + widget.setData("\ud800blah\b") + assertThat( + widget.binding.barcodeButton.text, + equalTo(widgetTestActivity.getString(string.replace_barcode)) + ) + } + + @Test + fun `#setData call #valueChangeListener`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener(widget) + widget.setData("blah") + + verify(valueChangedListener).widgetValueChanged(widget) + } + + @Test + fun `Long-pressing the button and the answer triggers #onLongClickListener`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + widget.setOnLongClickListener(listener) + widget.binding.barcodeButton.performLongClick() + widget.binding.answerView.performLongClick() + + verify(listener).onLongClick(widget.binding.barcodeButton) + verify(listener).onLongClick(widget.binding.answerView) + } + + @Test + fun `pressing the button with permission not granted does not launch anything`() { + val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + permissionsProvider.setPermissionGranted(false) + widget.setPermissionsProvider(permissionsProvider) + widget.binding.barcodeButton.performClick() + + assertThat(shadowActivity.nextStartedActivity, equalTo(null)) + assertThat(waitingForDataRegistry.waiting.isEmpty(), equalTo(true)) + } + + @Test + fun `pressing the button with permission granted registers widget for data waiting`() { + val prompt = QuestionWidgetHelpers.promptWithAnswer(null) + whenever(prompt.index).thenReturn(formIndex) + + val widget = createWidget(prompt) + widget.setPermissionsProvider(permissionsProvider) + widget.binding.barcodeButton.performClick() + + assertThat( + waitingForDataRegistry.waiting.contains(formIndex), + equalTo(true) + ) + } + + @Test + fun `pressing the button when front camera should be used but it is not available displays a toast`() { + whenever(cameraUtils.isFrontCameraAvailable(ArgumentMatchers.any())).thenReturn(false) + val widget = createWidget(QuestionWidgetHelpers.promptWithAppearance(Appearances.FRONT)) + widget.setPermissionsProvider(permissionsProvider) + widget.binding.barcodeButton.performClick() + + assertThat( + ShadowToast.getTextOfLatestToast(), + equalTo(widgetTestActivity.getString(string.error_front_camera_unavailable)) + ) + } + + @Test + fun `pressing the button when front camera should be used and it is available launches correct intent`() { + whenever(cameraUtils.isFrontCameraAvailable(ArgumentMatchers.any())).thenReturn(true) + val widget = createWidget(QuestionWidgetHelpers.promptWithAppearance(Appearances.FRONT)) + widget.setPermissionsProvider(permissionsProvider) + widget.binding.barcodeButton.performClick() + + assertThat( + shadowActivity.nextStartedActivity.getBooleanExtra(Appearances.FRONT, false), + equalTo(true) + ) + } + + @Test + fun `The answer is not displayed with hidden mode`() { + val prompt = + MockFormEntryPromptBuilder(QuestionWidgetHelpers.promptWithAppearance(Appearances.HIDDEN_ANSWER)) + .withAnswer(StringData("original contents")) + .build() + + val widget = createWidget(prompt) + + // Check initial value is not shown + assertThat( + widget.binding.answerView.findViewById(R.id.barcode_widget_answer).visibility, + equalTo(View.GONE) + ) + assertThat( + widget.binding.barcodeButton.text, + equalTo(widgetTestActivity.getString(string.replace_barcode)) + ) + assertThat(widget.answer, equalTo(StringData("original contents"))) + + // Check updates aren't shown + widget.setData("updated contents") + assertThat( + widget.binding.answerView.findViewById(R.id.barcode_widget_answer).visibility, + equalTo(View.GONE) + ) + assertThat( + widget.binding.barcodeButton.text, + equalTo(widgetTestActivity.getString(string.replace_barcode)) + ) + assertThat( + widget.answer, + equalTo(StringData("updated contents")) + ) + } + + private fun createWidget(prompt: FormEntryPrompt?) = BarcodeWidget( + widgetTestActivity, + QuestionDetails(prompt), + waitingForDataRegistry, + cameraUtils + ) +}