[Autofill Assistant] Added ForEach interaction.
Note: this is an alternative solution for http://crrev/c/2235698
This interaction executes a number of callbacks for the input loop value. This is intended to be used to inflate UI elements for client-only values, i.e., for values that the backend can't specify.
Internally, ForEach loops are implemented by introducing the concept of callback contexts, which will change value and view lookup accordingly.
In particular, callback contexts are used to automatically replace placeholders of the form ${i} in value and view identifiers (where 'i' is the loop identifier). This allows creating and referencing values and views with templated names, such as "created_view_${i}" and "value[${i}]".
Bug: b/145043394
Change-Id: I53089252fe1cc14b2b1fb74cfc56d7314bc4b37c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2241975
Commit-Queue: Clemens Arbesser <arbesser@google.com>
Reviewed-by: Sandro Maggi <sandromaggi@google.com>
Reviewed-by: Marian Fechete <marianfe@google.com>
Cr-Commit-Position: refs/heads/master@{#780785}
diff --git a/chrome/android/features/autofill_assistant/javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantGenericUiTest.java b/chrome/android/features/autofill_assistant/javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantGenericUiTest.java
index 256048c..ca5ae0c 100644
--- a/chrome/android/features/autofill_assistant/javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantGenericUiTest.java
+++ b/chrome/android/features/autofill_assistant/javatests/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantGenericUiTest.java
@@ -27,6 +27,7 @@
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -65,6 +66,7 @@
import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.autofill_assistant.generic_ui.AssistantDimension;
import org.chromium.chrome.browser.autofill_assistant.proto.ActionProto;
+import org.chromium.chrome.browser.autofill_assistant.proto.AutofillFormatProto;
import org.chromium.chrome.browser.autofill_assistant.proto.BooleanAndProto;
import org.chromium.chrome.browser.autofill_assistant.proto.BooleanList;
import org.chromium.chrome.browser.autofill_assistant.proto.BooleanNotProto;
@@ -77,7 +79,9 @@
import org.chromium.chrome.browser.autofill_assistant.proto.CollectUserDataResultProto;
import org.chromium.chrome.browser.autofill_assistant.proto.ColorProto;
import org.chromium.chrome.browser.autofill_assistant.proto.ComputeValueProto;
+import org.chromium.chrome.browser.autofill_assistant.proto.CreateCreditCardResponseProto;
import org.chromium.chrome.browser.autofill_assistant.proto.CreateNestedGenericUiProto;
+import org.chromium.chrome.browser.autofill_assistant.proto.CreditCardResponseProto;
import org.chromium.chrome.browser.autofill_assistant.proto.DateFormatProto;
import org.chromium.chrome.browser.autofill_assistant.proto.DateList;
import org.chromium.chrome.browser.autofill_assistant.proto.DateProto;
@@ -89,6 +93,7 @@
import org.chromium.chrome.browser.autofill_assistant.proto.EndActionProto;
import org.chromium.chrome.browser.autofill_assistant.proto.EventProto;
import org.chromium.chrome.browser.autofill_assistant.proto.FocusElementProto;
+import org.chromium.chrome.browser.autofill_assistant.proto.ForEachProto;
import org.chromium.chrome.browser.autofill_assistant.proto.GenericUserInterfaceProto;
import org.chromium.chrome.browser.autofill_assistant.proto.ImageViewProto;
import org.chromium.chrome.browser.autofill_assistant.proto.InfoPopupProto;
@@ -156,16 +161,19 @@
@Rule
public CustomTabActivityTestRule mTestRule = new CustomTabActivityTestRule();
+ private AutofillAssistantCollectUserDataTestHelper mHelper;
+
private static final String TEST_PAGE = "/components/test/data/autofill_assistant/html/"
+ "autofill_assistant_target_website.html";
@Before
- public void setUp() {
+ public void setUp() throws Exception {
AutofillAssistantPreferencesUtil.setInitialPreferences(true);
mTestRule.startCustomTabActivityWithIntent(CustomTabsTestUtils.createMinimalCustomTabIntent(
InstrumentationRegistry.getTargetContext(),
mTestRule.getTestServer().getURL(TEST_PAGE)));
mTestRule.getActivity().getScrim().disableAnimationForTesting(true);
+ mHelper = new AutofillAssistantCollectUserDataTestHelper();
}
private ViewProto createTestImage(String resourceId, String identifier) {
@@ -255,6 +263,44 @@
.build();
}
+ private ViewProto createSimpleTextView(String identifier, String text) {
+ return (ViewProto) ViewProto.newBuilder()
+ .setIdentifier(identifier)
+ .setTextView(TextViewProto.newBuilder().setText(text))
+ .build();
+ }
+
+ // A simple view that takes its text from the provided model identifier.
+ private ViewProto createTextModelView(String identifier, String modelIdentifier) {
+ return (ViewProto) ViewProto.newBuilder()
+ .setIdentifier(identifier)
+ .setTextView(TextViewProto.newBuilder().setModelIdentifier(modelIdentifier))
+ .build();
+ }
+
+ private CallbackProto createAutofillToStringCallback(
+ String inputModelIdentifier, String resultModelIdentifier, String autofillFormat) {
+ return (CallbackProto) CallbackProto.newBuilder()
+ .setComputeValue(
+ ComputeValueProto.newBuilder()
+ .setResultModelIdentifier(resultModelIdentifier)
+ .setToString(
+ ToStringProto.newBuilder()
+ .setValue(ValueReferenceProto.newBuilder()
+ .setModelIdentifier(
+ inputModelIdentifier))
+ .setAutofillFormat(
+ AutofillFormatProto.newBuilder().setPattern(
+ autofillFormat))))
+ .build();
+ }
+
+ private ValueReferenceProto createValueReference(String modelIdentifier) {
+ return (ValueReferenceProto) ValueReferenceProto.newBuilder()
+ .setModelIdentifier(modelIdentifier)
+ .build();
+ }
+
@Test
@MediumTest
@DisabledTest(message = "crbug.com/1033877")
@@ -2502,13 +2548,6 @@
onView(withText("center-aligned text")).check(matches(withTextGravity(Gravity.CENTER)));
}
- private ViewProto createSimpleTextView(String identifier, String text) {
- return (ViewProto) ViewProto.newBuilder()
- .setIdentifier(identifier)
- .setTextView(TextViewProto.newBuilder().setText(text))
- .build();
- }
-
/**
* Creates and deletes nested UIs. Also tests startup events for nested UIs.
*/
@@ -2711,4 +2750,407 @@
tapElement(mTestRule, "touch_area_one");
waitUntilViewMatchesCondition(withText("Prompt"), isCompletelyDisplayed());
}
+
+ /**
+ * Tests a simple for-each loop.
+ */
+ @Test
+ @MediumTest
+ public void testForEach() {
+ // Clicking the view will run a for-each loop that loops over a value and writes the i'th
+ // value to result_i, and then ends the action.
+ List<InteractionProto> interactions = new ArrayList<>();
+ interactions.add(
+ (InteractionProto) InteractionProto.newBuilder()
+ .setTriggerEvent(EventProto.newBuilder().setOnViewClicked(
+ OnViewClickedEventProto.newBuilder().setViewIdentifier(
+ "clickable_view")))
+ .addCallbacks(CallbackProto.newBuilder().setForEach(
+ ForEachProto.newBuilder()
+ .setLoopCounter("i")
+ .setLoopValueModelIdentifier("loop_value")
+ .addCallbacks(CallbackProto.newBuilder().setSetValue(
+ SetModelValueProto.newBuilder()
+ .setModelIdentifier("result_${i}")
+ .setValue(createValueReference(
+ "loop_value[${i}]"))))))
+ .addCallbacks(CallbackProto.newBuilder().setEndAction(
+ EndActionProto.newBuilder().setStatus(
+ ProcessedActionStatusProto.ACTION_APPLIED)))
+ .build());
+
+ List<ModelProto.ModelValue> modelValues = new ArrayList<>();
+ modelValues.add((ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("loop_value")
+ .setValue(ValueProto.newBuilder().setStrings(
+ StringList.newBuilder().addAllValues(
+ Arrays.asList("first", "second", "third"))))
+ .build());
+ modelValues.add((ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("result_0")
+ .build());
+ modelValues.add((ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("result_1")
+ .build());
+ modelValues.add((ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("result_2")
+ .build());
+
+ GenericUserInterfaceProto genericUserInterface =
+ (GenericUserInterfaceProto) GenericUserInterfaceProto.newBuilder()
+ .setRootView(ViewProto.newBuilder()
+ .setIdentifier("clickable_view")
+ .setTextView(TextViewProto.newBuilder().setText(
+ "Click me")))
+ .setInteractions(
+ InteractionsProto.newBuilder().addAllInteractions(interactions))
+ .setModel(ModelProto.newBuilder().addAllValues(modelValues))
+ .build();
+
+ ArrayList<ActionProto> list = new ArrayList<>();
+ list.add((ActionProto) ActionProto.newBuilder()
+ .setShowGenericUi(ShowGenericUiProto.newBuilder()
+ .setGenericUserInterface(genericUserInterface)
+ .addAllOutputModelIdentifiers(Arrays.asList(
+ "result_0", "result_1", "result_2")))
+ .build());
+ AutofillAssistantTestScript script = new AutofillAssistantTestScript(
+ (SupportedScriptProto) SupportedScriptProto.newBuilder()
+ .setPath("autofill_assistant_target_website.html")
+ .setPresentation(PresentationProto.newBuilder().setAutostart(true).setChip(
+ ChipProto.newBuilder().setText("Autostart")))
+ .build(),
+ list);
+
+ AutofillAssistantTestService testService =
+ new AutofillAssistantTestService(Collections.singletonList(script));
+ startAutofillAssistant(mTestRule.getActivity(), testService);
+
+ waitUntilViewMatchesCondition(withText("Click me"), isCompletelyDisplayed());
+
+ int numNextActionsCalled = testService.getNextActionsCounter();
+ onView(withText("Click me")).perform(click());
+ testService.waitUntilGetNextActions(numNextActionsCalled + 1);
+
+ List<ProcessedActionProto> processedActions = testService.getProcessedActions();
+ assertThat(processedActions, iterableWithSize(1));
+ assertThat(
+ processedActions.get(0).getStatus(), is(ProcessedActionStatusProto.ACTION_APPLIED));
+ ShowGenericUiProto.Result result = processedActions.get(0).getShowGenericUiResult();
+ List<ModelProto.ModelValue> resultModelValues = result.getModel().getValuesList();
+ assertThat(resultModelValues, iterableWithSize(3));
+ assertThat(resultModelValues,
+ containsInAnyOrder((ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("result_0")
+ .setValue(ValueProto.newBuilder().setStrings(
+ StringList.newBuilder().addValues("first")))
+ .build(),
+ (ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("result_1")
+ .setValue(ValueProto.newBuilder().setStrings(
+ StringList.newBuilder().addValues("second")))
+ .build(),
+ (ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("result_2")
+ .setValue(ValueProto.newBuilder().setStrings(
+ StringList.newBuilder().addValues("third")))
+ .build()));
+ }
+
+ /**
+ * Tests a nested for-each loop.
+ */
+ @Test
+ @MediumTest
+ public void testNestedForEach() {
+ // In pseudo code:
+ // items = {"first", "second", "third"};
+ // for (int i = 0; i < items.size(); i++) {
+ // for (int j = 0; j < items.size(); j++) {
+ // result_i_j = items[j];
+ // }
+ // }
+ //
+ // Which should result in:
+ // result_0_0 = first
+ // result_0_1 = second
+ // result_0_2 = third
+ // result_1_0 = first
+ // ...
+ // result_2_2 = third
+ //
+ CallbackProto nestedForEach =
+ (CallbackProto) CallbackProto.newBuilder()
+ .setForEach(ForEachProto.newBuilder()
+ .setLoopCounter("j")
+ .setLoopValueModelIdentifier("loop_value")
+ .addCallbacks(CallbackProto.newBuilder().setSetValue(
+ SetModelValueProto.newBuilder()
+ .setModelIdentifier("result_${i}_${j}")
+ .setValue(createValueReference(
+ "loop_value[${j}]")))))
+ .build();
+
+ List<InteractionProto> interactions = new ArrayList<>();
+ interactions.add((InteractionProto) InteractionProto.newBuilder()
+ .setTriggerEvent(EventProto.newBuilder().setOnViewClicked(
+ OnViewClickedEventProto.newBuilder().setViewIdentifier(
+ "clickable_view")))
+ .addCallbacks(CallbackProto.newBuilder().setForEach(
+ ForEachProto.newBuilder()
+ .setLoopCounter("i")
+ .setLoopValueModelIdentifier("loop_value")
+ .addCallbacks(nestedForEach)))
+ .addCallbacks(CallbackProto.newBuilder().setEndAction(
+ EndActionProto.newBuilder().setStatus(
+ ProcessedActionStatusProto.ACTION_APPLIED)))
+ .build());
+
+ List<String> loopValue = Arrays.asList("first", "second", "third");
+ List<ModelProto.ModelValue> modelValues = new ArrayList<>();
+ modelValues.add((ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("loop_value")
+ .setValue(ValueProto.newBuilder().setStrings(
+ StringList.newBuilder().addAllValues(loopValue)))
+ .build());
+
+ List<String> outputModelIdentifiers = new ArrayList<>();
+ for (int i = 0; i < loopValue.size(); i++) {
+ for (int j = 0; j < loopValue.size(); j++) {
+ String identifier = "result_" + i + "_" + j;
+ modelValues.add((ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier(identifier)
+ .build());
+ outputModelIdentifiers.add(identifier);
+ }
+ }
+
+ GenericUserInterfaceProto genericUserInterface =
+ (GenericUserInterfaceProto) GenericUserInterfaceProto.newBuilder()
+ .setRootView(ViewProto.newBuilder()
+ .setIdentifier("clickable_view")
+ .setTextView(TextViewProto.newBuilder().setText(
+ "Click me")))
+ .setInteractions(
+ InteractionsProto.newBuilder().addAllInteractions(interactions))
+ .setModel(ModelProto.newBuilder().addAllValues(modelValues))
+ .build();
+
+ ArrayList<ActionProto> list = new ArrayList<>();
+ list.add((ActionProto) ActionProto.newBuilder()
+ .setShowGenericUi(
+ ShowGenericUiProto.newBuilder()
+ .setGenericUserInterface(genericUserInterface)
+ .addAllOutputModelIdentifiers(outputModelIdentifiers))
+ .build());
+ AutofillAssistantTestScript script = new AutofillAssistantTestScript(
+ (SupportedScriptProto) SupportedScriptProto.newBuilder()
+ .setPath("autofill_assistant_target_website.html")
+ .setPresentation(PresentationProto.newBuilder().setAutostart(true).setChip(
+ ChipProto.newBuilder().setText("Autostart")))
+ .build(),
+ list);
+
+ AutofillAssistantTestService testService =
+ new AutofillAssistantTestService(Collections.singletonList(script));
+ startAutofillAssistant(mTestRule.getActivity(), testService);
+
+ waitUntilViewMatchesCondition(withText("Click me"), isCompletelyDisplayed());
+
+ int numNextActionsCalled = testService.getNextActionsCounter();
+ onView(withText("Click me")).perform(click());
+ testService.waitUntilGetNextActions(numNextActionsCalled + 1);
+
+ List<ProcessedActionProto> processedActions = testService.getProcessedActions();
+ assertThat(processedActions, iterableWithSize(1));
+ assertThat(
+ processedActions.get(0).getStatus(), is(ProcessedActionStatusProto.ACTION_APPLIED));
+ ShowGenericUiProto.Result result = processedActions.get(0).getShowGenericUiResult();
+ List<ModelProto.ModelValue> resultModelValues = result.getModel().getValuesList();
+
+ List<ModelProto.ModelValue> expectedResultValues = new ArrayList<>();
+ for (int i = 0; i < loopValue.size(); i++) {
+ for (int j = 0; j < loopValue.size(); j++) {
+ expectedResultValues.add(
+ (ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("result_" + i + "_" + j)
+ .setValue(ValueProto.newBuilder().setStrings(
+ StringList.newBuilder().addValues(loopValue.get(j))))
+ .build());
+ }
+ }
+ assertThat(resultModelValues, iterableWithSize(expectedResultValues.size()));
+ assertThat(resultModelValues, containsInAnyOrder(expectedResultValues.toArray()));
+ }
+
+ /**
+ * Shows a simple UI (one view per credit card).
+ */
+ @Test
+ @MediumTest
+ public void testCreditCardUi() throws Exception {
+ // Clicking |credit_card_view| will write the current card to |selected_card| and end the
+ // action.
+ List<InteractionProto> singleCardInteractions = new ArrayList<>();
+ singleCardInteractions.add(
+ (InteractionProto) InteractionProto.newBuilder()
+ .setTriggerEvent(EventProto.newBuilder().setOnViewClicked(
+ OnViewClickedEventProto.newBuilder().setViewIdentifier(
+ "credit_card_view_${i}")))
+ .addCallbacks(CallbackProto.newBuilder().setSetValue(
+ SetModelValueProto.newBuilder()
+ .setModelIdentifier("selected_credit_card")
+ .setValue(createValueReference("credit_cards[${i}]"))))
+ .addCallbacks(CallbackProto.newBuilder().setEndAction(
+ EndActionProto.newBuilder().setStatus(
+ ProcessedActionStatusProto.ACTION_APPLIED)))
+ .build());
+
+ // For each credit card, a simple UI containing the name and the obfuscated number is
+ // created.
+ GenericUserInterfaceProto singleCardUi =
+ (GenericUserInterfaceProto) GenericUserInterfaceProto.newBuilder()
+ .setRootView(
+ ViewProto.newBuilder()
+ .setIdentifier("credit_card_view_${i}")
+ .setViewContainer(
+ ViewContainerProto.newBuilder()
+ .setLinearLayout(
+ LinearLayoutProto.newBuilder()
+ .setOrientation(
+ LinearLayoutProto
+ .Orientation
+ .VERTICAL))
+ .addViews(createTextModelView(
+ "card_holder_name_view_${i}",
+ "card_holder_name_${i}"))
+ .addViews(createTextModelView(
+ "obfuscated_number_view_${i}",
+ "obfuscated_number_${i}"))))
+ .setInteractions(InteractionsProto.newBuilder().addAllInteractions(
+ singleCardInteractions))
+
+ .build();
+
+ // Every time |credit_cards| changes, we:
+ // - clear any previous card views
+ // - compute |card_holder_name_${i}| and |obfuscated_number_${i}|
+ // - re-create card UI
+ List<InteractionProto> interactions = new ArrayList<>();
+ interactions.add(
+ (InteractionProto) InteractionProto.newBuilder()
+ .setTriggerEvent(EventProto.newBuilder().setOnValueChanged(
+ OnModelValueChangedEventProto.newBuilder().setModelIdentifier(
+ "credit_cards")))
+ .addCallbacks(CallbackProto.newBuilder().setClearViewContainer(
+ ClearViewContainerProto.newBuilder().setViewIdentifier(
+ "credit_card_container_view")))
+ .addCallbacks(CallbackProto.newBuilder().setForEach(
+ ForEachProto.newBuilder()
+ .setLoopCounter("i")
+ .setLoopValueModelIdentifier("credit_cards")
+ .addCallbacks(
+ createAutofillToStringCallback("credit_cards[${i}]",
+ "card_holder_name_${i}", "${51}"))
+ .addCallbacks(
+ createAutofillToStringCallback("credit_cards[${i}]",
+ "obfuscated_number_${i}", "•••• ${-4}"))
+ .addCallbacks(CallbackProto.newBuilder().setCreateNestedUi(
+ CreateNestedGenericUiProto.newBuilder()
+ .setGenericUiIdentifier("nested_ui_${i}")
+ .setGenericUi(singleCardUi)
+ .setParentViewIdentifier(
+ "credit_card_container_view")))))
+ .build());
+
+ // Every time |selected_credit_card| changes, we write the network of the selected card to
+ // |selected_card_network|, which will be sent back to backend.
+ interactions.add(
+ (InteractionProto) InteractionProto.newBuilder()
+ .setTriggerEvent(EventProto.newBuilder().setOnValueChanged(
+ OnModelValueChangedEventProto.newBuilder().setModelIdentifier(
+ "selected_credit_card")))
+ .addCallbacks(CallbackProto.newBuilder().setComputeValue(
+ ComputeValueProto.newBuilder()
+ .setResultModelIdentifier("selected_card_network")
+ .setCreateCreditCardResponse(
+ CreateCreditCardResponseProto.newBuilder().setValue(
+ createValueReference(
+ "selected_credit_card")))))
+ .build());
+
+ List<ModelProto.ModelValue> modelValues = new ArrayList<>();
+ modelValues.add((ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("selected_card_network")
+ .build());
+
+ GenericUserInterfaceProto genericUserInterface =
+ (GenericUserInterfaceProto) GenericUserInterfaceProto.newBuilder()
+ .setRootView(
+ ViewProto.newBuilder()
+ .setIdentifier("credit_card_container_view")
+ .setViewContainer(
+ ViewContainerProto.newBuilder().setLinearLayout(
+ LinearLayoutProto.newBuilder()
+ .setOrientation(
+ LinearLayoutProto
+ .Orientation
+ .VERTICAL))))
+ .setInteractions(
+ InteractionsProto.newBuilder().addAllInteractions(interactions))
+ .setModel(ModelProto.newBuilder().addAllValues(modelValues))
+ .build();
+
+ ArrayList<ActionProto> list = new ArrayList<>();
+ list.add((ActionProto) ActionProto.newBuilder()
+ .setShowGenericUi(
+ ShowGenericUiProto.newBuilder()
+ .setGenericUserInterface(genericUserInterface)
+ .setRequestCreditCards(
+ ShowGenericUiProto.RequestAutofillCreditCards
+ .newBuilder()
+ .setModelIdentifier("credit_cards"))
+ .addOutputModelIdentifiers("selected_card_network"))
+ .build());
+ AutofillAssistantTestScript script = new AutofillAssistantTestScript(
+ (SupportedScriptProto) SupportedScriptProto.newBuilder()
+ .setPath("autofill_assistant_target_website.html")
+ .setPresentation(PresentationProto.newBuilder().setAutostart(true).setChip(
+ ChipProto.newBuilder().setText("Autostart")))
+ .build(),
+ list);
+
+ mHelper.addDummyCreditCard(mHelper.addDummyProfile("John Doe", "johndoe@google.com"));
+ mHelper.addDummyCreditCard(mHelper.addDummyProfile("Jane Doe", "janedoe@google.com"));
+
+ AutofillAssistantTestService testService =
+ new AutofillAssistantTestService(Collections.singletonList(script));
+ startAutofillAssistant(mTestRule.getActivity(), testService);
+
+ waitUntilViewMatchesCondition(withText("John Doe"), isCompletelyDisplayed());
+ onView(withText("Jane Doe")).check(matches(isDisplayed()));
+ onView(allOf(withText(containsString("1111")), hasSibling(withText("John Doe"))))
+ .check(matches(isDisplayed()));
+ onView(allOf(withText(containsString("1111")), hasSibling(withText("Jane Doe"))))
+ .check(matches(isDisplayed()));
+
+ int numNextActionsCalled = testService.getNextActionsCounter();
+ onView(withText("Jane Doe")).perform(click());
+ testService.waitUntilGetNextActions(numNextActionsCalled + 1);
+
+ List<ProcessedActionProto> processedActions = testService.getProcessedActions();
+ assertThat(processedActions, iterableWithSize(1));
+ assertThat(
+ processedActions.get(0).getStatus(), is(ProcessedActionStatusProto.ACTION_APPLIED));
+ ShowGenericUiProto.Result result = processedActions.get(0).getShowGenericUiResult();
+ List<ModelProto.ModelValue> resultModelValues = result.getModel().getValuesList();
+ assertThat(resultModelValues, iterableWithSize(1));
+ assertThat(resultModelValues,
+ containsInAnyOrder(
+ (ModelProto.ModelValue) ModelProto.ModelValue.newBuilder()
+ .setIdentifier("selected_card_network")
+ .setValue(ValueProto.newBuilder().setCreditCardResponse(
+ CreditCardResponseProto.newBuilder().setNetwork("visa")))
+ .build()));
+ }
}
diff --git a/chrome/browser/android/autofill_assistant/generic_ui_controller_android.cc b/chrome/browser/android/autofill_assistant/generic_ui_controller_android.cc
index f5a8d0f..045e11d 100644
--- a/chrome/browser/android/autofill_assistant/generic_ui_controller_android.cc
+++ b/chrome/browser/android/autofill_assistant/generic_ui_controller_android.cc
@@ -489,16 +489,17 @@
std::unique_ptr<GenericUiControllerAndroid>
GenericUiControllerAndroid::CreateFromProto(
const GenericUserInterfaceProto& proto,
+ const std::map<std::string, std::string> context,
base::android::ScopedJavaGlobalRef<jobject> jcontext,
base::android::ScopedJavaGlobalRef<jobject> jdelegate,
EventHandler* event_handler,
UserModel* user_model,
BasicInteractions* basic_interactions) {
// Create view layout.
- auto view_handler = std::make_unique<ViewHandlerAndroid>();
+ auto view_handler = std::make_unique<ViewHandlerAndroid>(context);
auto interaction_handler = std::make_unique<InteractionHandlerAndroid>(
- event_handler, user_model, basic_interactions, view_handler.get(),
- jcontext, jdelegate);
+ context, event_handler, user_model, basic_interactions,
+ view_handler.get(), jcontext, jdelegate);
JNIEnv* env = base::android::AttachCurrentThread();
auto jroot_view =
proto.has_root_view()
diff --git a/chrome/browser/android/autofill_assistant/generic_ui_controller_android.h b/chrome/browser/android/autofill_assistant/generic_ui_controller_android.h
index e50dfc26..f741954 100644
--- a/chrome/browser/android/autofill_assistant/generic_ui_controller_android.h
+++ b/chrome/browser/android/autofill_assistant/generic_ui_controller_android.h
@@ -28,6 +28,7 @@
// Ownership of the arguments is not changed.
static std::unique_ptr<GenericUiControllerAndroid> CreateFromProto(
const GenericUserInterfaceProto& proto,
+ const std::map<std::string, std::string> context,
base::android::ScopedJavaGlobalRef<jobject> jcontext,
base::android::ScopedJavaGlobalRef<jobject> jdelegate,
EventHandler* event_handler,
diff --git a/chrome/browser/android/autofill_assistant/interaction_handler_android.cc b/chrome/browser/android/autofill_assistant/interaction_handler_android.cc
index 90d4fd26..79017b3 100644
--- a/chrome/browser/android/autofill_assistant/interaction_handler_android.cc
+++ b/chrome/browser/android/autofill_assistant/interaction_handler_android.cc
@@ -14,6 +14,7 @@
#include "chrome/browser/android/autofill_assistant/generic_ui_interactions_android.h"
#include "chrome/browser/android/autofill_assistant/view_handler_android.h"
#include "components/autofill_assistant/browser/basic_interactions.h"
+#include "components/autofill_assistant/browser/field_formatter.h"
#include "components/autofill_assistant/browser/generic_ui.pb.h"
#include "components/autofill_assistant/browser/ui_delegate.h"
#include "components/autofill_assistant/browser/user_model.h"
@@ -21,14 +22,113 @@
namespace autofill_assistant {
+namespace {
+
+// Helper RAII class that sets the execution context for callbacks and unsets
+// the context upon deletion. Simply unsetting the context after running the
+// callbacks is unsafe, as a callback may have ended the action, thus deleting
+// the context and leading to a crash.
+class SetExecutionContext {
+ public:
+ SetExecutionContext(base::WeakPtr<UserModel> user_model,
+ base::WeakPtr<ViewHandlerAndroid> view_handler,
+ const std::map<std::string, std::string>& context)
+ : user_model_(user_model),
+ view_handler_(view_handler),
+ context_(context) {
+ if (user_model_ != nullptr) {
+ user_model_->AddIdentifierPlaceholders(context_);
+ }
+ if (view_handler_ != nullptr) {
+ view_handler_->AddIdentifierPlaceholders(context_);
+ }
+ }
+
+ ~SetExecutionContext() {
+ if (user_model_ != nullptr) {
+ user_model_->RemoveIdentifierPlaceholders(context_);
+ }
+ if (view_handler_ != nullptr) {
+ view_handler_->RemoveIdentifierPlaceholders(context_);
+ }
+ }
+
+ private:
+ base::WeakPtr<UserModel> user_model_;
+ base::WeakPtr<ViewHandlerAndroid> view_handler_;
+ std::map<std::string, std::string> context_;
+};
+
+// Runs |callbacks| using the context provided by |interaction_handler| and
+// |additional_context|.
+// Note: parameters are passed by value, as their owner may go out of scope
+// before all callbacks have been processed.
+void RunWithContext(
+ std::vector<InteractionHandlerAndroid::InteractionCallback> callbacks,
+ std::map<std::string, std::string> additional_context,
+ base::WeakPtr<InteractionHandlerAndroid> interaction_handler,
+ base::WeakPtr<UserModel> user_model,
+ base::WeakPtr<ViewHandlerAndroid> view_handler) {
+ if (!interaction_handler || !user_model || !view_handler) {
+ return;
+ }
+
+ // Context is set via RAII to ensure that it is properly unset when done.
+ interaction_handler->AddContext(additional_context);
+ SetExecutionContext set_context(user_model, view_handler,
+ interaction_handler->GetContext());
+ for (const auto& callback : callbacks) {
+ callback.Run();
+ // A callback may have caused |interaction_handler| to go out of scope.
+ if (!interaction_handler) {
+ return;
+ }
+ }
+ if (interaction_handler != nullptr) {
+ interaction_handler->RemoveContext(additional_context);
+ }
+}
+
+void RunForEachLoop(
+ const ForEachProto& proto,
+ const std::vector<InteractionHandlerAndroid::InteractionCallback>&
+ callbacks,
+ base::WeakPtr<InteractionHandlerAndroid> interaction_handler,
+ base::WeakPtr<UserModel> user_model,
+ base::WeakPtr<ViewHandlerAndroid> view_handler) {
+ if (!interaction_handler || !user_model || !view_handler) {
+ return;
+ }
+ auto loop_value = user_model->GetValue(proto.loop_value_model_identifier());
+ if (!loop_value.has_value()) {
+ VLOG(2) << "Error running ForEach loop: "
+ << proto.loop_value_model_identifier() << " not found in model";
+ return;
+ }
+
+ for (int i = 0; i < GetValueSize(*loop_value); ++i) {
+ // Temporarily add "<loop_counter> -> i" to execution context.
+ // Note: interactions may create nested UI instances. Those instances
+ // will inherit their parents' current context, which includes the
+ // placeholder for the loop variable currently being iterated.
+ RunWithContext(callbacks, /* additional_context = */
+ {{proto.loop_counter(), base::NumberToString(i)}},
+ interaction_handler, user_model, view_handler);
+ }
+}
+
+} // namespace
+
InteractionHandlerAndroid::InteractionHandlerAndroid(
+ const std::map<std::string, std::string>& context,
EventHandler* event_handler,
UserModel* user_model,
BasicInteractions* basic_interactions,
ViewHandlerAndroid* view_handler,
base::android::ScopedJavaGlobalRef<jobject> jcontext,
base::android::ScopedJavaGlobalRef<jobject> jdelegate)
- : event_handler_(event_handler),
+ : context_(context),
+ event_handler_(event_handler),
user_model_(user_model),
basic_interactions_(basic_interactions),
view_handler_(view_handler),
@@ -54,6 +154,20 @@
is_listening_ = false;
}
+void InteractionHandlerAndroid::AddContext(
+ const std::map<std::string, std::string>& context) {
+ for (const auto& value : context) {
+ context_[value.first] = value.second;
+ }
+}
+
+void InteractionHandlerAndroid::RemoveContext(
+ const std::map<std::string, std::string>& context) {
+ for (const auto& value : context) {
+ context_.erase(value.first);
+ }
+}
+
UserModel* InteractionHandlerAndroid::GetUserModel() const {
return user_model_;
}
@@ -101,9 +215,11 @@
void InteractionHandlerAndroid::OnEvent(const EventHandler::EventKey& key) {
auto it = interactions_.find(key);
if (it != interactions_.end()) {
- for (auto& callback : it->second) {
- callback.Run();
- }
+ RunWithContext(it->second, /* additional_context = */ {},
+ this->GetWeakPtr(), user_model_->GetWeakPtr(),
+ view_handler_->GetWeakPtr());
+ // Note: it is unsafe to call any code after running callbacks, because
+ // a callback may effectively delete *this.
}
}
@@ -277,13 +393,45 @@
base::BindRepeating(&android_interactions::ClearViewContainer,
proto.clear_view_container().view_identifier(),
view_handler_, jdelegate_));
+ case CallbackProto::kForEach: {
+ if (proto.for_each().loop_counter().empty()) {
+ VLOG(1) << "Error creating ForEach interaction: "
+ "loop_counter not set";
+ return base::nullopt;
+ }
+ if (proto.for_each().loop_value_model_identifier().empty()) {
+ VLOG(1) << "Error creating ForEach interaction: "
+ "loop_value_model_identifier not set";
+ return base::nullopt;
+ }
+ std::vector<InteractionHandlerAndroid::InteractionCallback> callbacks;
+ for (const auto& callback_proto : proto.for_each().callbacks()) {
+ auto callback = CreateInteractionCallbackFromProto(callback_proto);
+ if (!callback.has_value()) {
+ VLOG(1) << "Error creating ForEach interaction: failed to create "
+ "callback";
+ return base::nullopt;
+ }
+ callbacks.emplace_back(*callback);
+ }
+ return base::Optional<InteractionCallback>(base::BindRepeating(
+ &RunForEachLoop, proto.for_each(), callbacks, GetWeakPtr(),
+ user_model_->GetWeakPtr(), view_handler_->GetWeakPtr()));
+ }
case CallbackProto::KIND_NOT_SET:
VLOG(1) << "Error creating interaction: kind not set";
return base::nullopt;
}
}
-void InteractionHandlerAndroid::DeleteNestedUi(const std::string& identifier) {
+void InteractionHandlerAndroid::DeleteNestedUi(const std::string& input) {
+ // Replace all placeholders in the input.
+ auto formatted_identifier = field_formatter::FormatString(input, context_);
+ if (!formatted_identifier.has_value()) {
+ VLOG(2) << "Error deleting nested UI: placeholder not found for " << input;
+ return;
+ }
+ std::string identifier = *formatted_identifier;
auto it = nested_ui_controllers_.find(identifier);
if (it != nested_ui_controllers_.end()) {
nested_ui_controllers_.erase(it);
@@ -292,7 +440,14 @@
const GenericUiControllerAndroid* InteractionHandlerAndroid::CreateNestedUi(
const GenericUserInterfaceProto& proto,
- const std::string& identifier) {
+ const std::string& input) {
+ // Replace all placeholders in the input.
+ auto formatted_identifier = field_formatter::FormatString(input, context_);
+ if (!formatted_identifier.has_value()) {
+ VLOG(2) << "Error creating nested UI: placeholder not found for " << input;
+ return nullptr;
+ }
+ std::string identifier = *formatted_identifier;
if (nested_ui_controllers_.find(identifier) != nested_ui_controllers_.end()) {
VLOG(2) << "Error creating nested UI: " << identifier
<< " already exixsts (did you forget to clear the previous "
@@ -300,7 +455,7 @@
return nullptr;
}
auto nested_ui = GenericUiControllerAndroid::CreateFromProto(
- proto, jcontext_, jdelegate_, event_handler_, user_model_,
+ proto, context_, jcontext_, jdelegate_, event_handler_, user_model_,
basic_interactions_);
const auto* nested_ui_ptr = nested_ui.get();
if (nested_ui) {
diff --git a/chrome/browser/android/autofill_assistant/interaction_handler_android.h b/chrome/browser/android/autofill_assistant/interaction_handler_android.h
index 691d5bc7..ed6b5072 100644
--- a/chrome/browser/android/autofill_assistant/interaction_handler_android.h
+++ b/chrome/browser/android/autofill_assistant/interaction_handler_android.h
@@ -36,6 +36,7 @@
// Constructor. All dependencies must outlive this instance.
InteractionHandlerAndroid(
+ const std::map<std::string, std::string>& context,
EventHandler* event_handler,
UserModel* user_model,
BasicInteractions* basic_interactions,
@@ -49,6 +50,15 @@
void StartListening();
void StopListening();
+ // Adds |context| to the current context of this interaction handler.
+ void AddContext(const std::map<std::string, std::string>& context);
+
+ // Removes the keys in |context| from this handler's context.
+ void RemoveContext(const std::map<std::string, std::string>& context);
+
+ // Returns a copy of the current context.
+ std::map<std::string, std::string> GetContext() const { return context_; }
+
// Access to the user model that this interaction handler is bound to.
UserModel* GetUserModel() const;
@@ -103,6 +113,11 @@
std::map<EventHandler::EventKey, std::vector<InteractionCallback>>
interactions_;
+ // These key-value pairs specify context variables that the handler will use
+ // to resolve views and values. Nested instances will inherit their parents'
+ // context variables. Special interactions, such as ForEach, may modify the
+ // context while they are being executed.
+ std::map<std::string, std::string> context_;
EventHandler* event_handler_ = nullptr;
UserModel* user_model_ = nullptr;
BasicInteractions* basic_interactions_ = nullptr;
diff --git a/chrome/browser/android/autofill_assistant/ui_controller_android.cc b/chrome/browser/android/autofill_assistant/ui_controller_android.cc
index ddfe2144..f9fabecc6 100644
--- a/chrome/browser/android/autofill_assistant/ui_controller_android.cc
+++ b/chrome/browser/android/autofill_assistant/ui_controller_android.cc
@@ -1691,7 +1691,8 @@
auto jcontext =
Java_AutofillAssistantUiController_getContext(env, java_object_);
return GenericUiControllerAndroid::CreateFromProto(
- proto, base::android::ScopedJavaGlobalRef<jobject>(jcontext),
+ proto, /* context = */ {},
+ base::android::ScopedJavaGlobalRef<jobject>(jcontext),
generic_ui_delegate_.GetJavaObject(), ui_delegate_->GetEventHandler(),
ui_delegate_->GetUserModel(), ui_delegate_->GetBasicInteractions());
}
diff --git a/chrome/browser/android/autofill_assistant/view_handler_android.cc b/chrome/browser/android/autofill_assistant/view_handler_android.cc
index 49a72a5c..982c71e0 100644
--- a/chrome/browser/android/autofill_assistant/view_handler_android.cc
+++ b/chrome/browser/android/autofill_assistant/view_handler_android.cc
@@ -3,14 +3,28 @@
// found in the LICENSE file.
#include "chrome/browser/android/autofill_assistant/view_handler_android.h"
+#include "components/autofill_assistant/browser/field_formatter.h"
namespace autofill_assistant {
-ViewHandlerAndroid::ViewHandlerAndroid() = default;
+ViewHandlerAndroid::ViewHandlerAndroid(
+ const std::map<std::string, std::string>& identifier_placeholders)
+ : identifier_placeholders_(identifier_placeholders) {}
ViewHandlerAndroid::~ViewHandlerAndroid() = default;
+base::WeakPtr<ViewHandlerAndroid> ViewHandlerAndroid::GetWeakPtr() {
+ return weak_ptr_factory_.GetWeakPtr();
+}
+
base::Optional<base::android::ScopedJavaGlobalRef<jobject>>
-ViewHandlerAndroid::GetView(const std::string& view_identifier) const {
+ViewHandlerAndroid::GetView(const std::string& input) const {
+ // Replace all placeholders in the input.
+ auto formatted_identifier =
+ field_formatter::FormatString(input, identifier_placeholders_);
+ if (!formatted_identifier.has_value()) {
+ return base::nullopt;
+ }
+ std::string view_identifier = *formatted_identifier;
auto it = views_.find(view_identifier);
if (it == views_.end()) {
return base::nullopt;
@@ -20,10 +34,31 @@
// Adds a view to the set of managed views.
void ViewHandlerAndroid::AddView(
- const std::string& view_identifier,
+ const std::string& input,
base::android::ScopedJavaGlobalRef<jobject> jview) {
+ // Replace all placeholders in the input.
+ auto formatted_identifier =
+ field_formatter::FormatString(input, identifier_placeholders_);
+ if (!formatted_identifier.has_value()) {
+ return;
+ }
+ std::string view_identifier = *formatted_identifier;
DCHECK(views_.find(view_identifier) == views_.end());
views_.emplace(view_identifier, jview);
}
+void ViewHandlerAndroid::AddIdentifierPlaceholders(
+ const std::map<std::string, std::string> placeholders) {
+ for (const auto& placeholder : placeholders) {
+ identifier_placeholders_[placeholder.first] = placeholder.second;
+ }
+}
+
+void ViewHandlerAndroid::RemoveIdentifierPlaceholders(
+ const std::map<std::string, std::string> placeholders) {
+ for (const auto& placeholder : placeholders) {
+ identifier_placeholders_.erase(placeholder.first);
+ }
+}
+
} // namespace autofill_assistant
diff --git a/chrome/browser/android/autofill_assistant/view_handler_android.h b/chrome/browser/android/autofill_assistant/view_handler_android.h
index 7bfc265..8433e87 100644
--- a/chrome/browser/android/autofill_assistant/view_handler_android.h
+++ b/chrome/browser/android/autofill_assistant/view_handler_android.h
@@ -10,6 +10,7 @@
#include <string>
#include "base/android/jni_android.h"
+#include "base/memory/weak_ptr.h"
#include "base/optional.h"
namespace autofill_assistant {
@@ -17,13 +18,18 @@
// Manages a map of view-identifier -> android view instances.
class ViewHandlerAndroid {
public:
- ViewHandlerAndroid();
+ explicit ViewHandlerAndroid(
+ const std::map<std::string, std::string>& identifier_placeholders);
~ViewHandlerAndroid();
ViewHandlerAndroid(const ViewHandlerAndroid&) = delete;
ViewHandlerAndroid& operator=(const ViewHandlerAndroid&) = delete;
+ base::WeakPtr<ViewHandlerAndroid> GetWeakPtr();
+
// Returns the view associated with |view_identifier| or base::nullopt if
// there is no such view.
+ // -Placeholders in |view_identifier| of the form ${key} are automatically
+ // replaced (see |AddIdentifierPlaceholders|).
base::Optional<base::android::ScopedJavaGlobalRef<jobject>> GetView(
const std::string& view_identifier) const;
@@ -31,8 +37,21 @@
void AddView(const std::string& view_identifier,
base::android::ScopedJavaGlobalRef<jobject> jview);
+ // Adds a set of placeholders (overwrite if necessary). When looking up views
+ // by identifier, all occurrences of ${key} are automatically replaced by
+ // their value. Example: the current set of placeholders contains "i" -> "1".
+ // Looking up the view "view_${i}" will now actually lookup "view_1".
+ void AddIdentifierPlaceholders(
+ const std::map<std::string, std::string> placeholders);
+
+ // Removes a set of placeholders.
+ void RemoveIdentifierPlaceholders(
+ const std::map<std::string, std::string> placeholders);
+
private:
std::map<std::string, base::android::ScopedJavaGlobalRef<jobject>> views_;
+ std::map<std::string, std::string> identifier_placeholders_;
+ base::WeakPtrFactory<ViewHandlerAndroid> weak_ptr_factory_{this};
};
} // namespace autofill_assistant
diff --git a/components/autofill_assistant/browser/generic_ui.proto b/components/autofill_assistant/browser/generic_ui.proto
index e5e93afd..85cbffb 100644
--- a/components/autofill_assistant/browser/generic_ui.proto
+++ b/components/autofill_assistant/browser/generic_ui.proto
@@ -51,6 +51,7 @@
ShowGenericUiPopupProto show_generic_popup = 13;
CreateNestedGenericUiProto create_nested_ui = 14;
ClearViewContainerProto clear_view_container = 15;
+ ForEachProto for_each = 16;
}
// Optional model identifier pointing to a single boolean. If set, the
// callback will only be invoked if the condition is true.
@@ -392,3 +393,18 @@
// The view container to clear.
optional string view_identifier = 1;
}
+
+// Invokes |callbacks| for each item in the loop value. Automatically replaces
+// instances of "${i}" in model and view identifiers with the loop counter.
+message ForEachProto {
+ // The loop counter, usually "i", "j", etc. Callbacks may use this counter
+ // in view and model identifiers by using "${i}"" placeholders, e.g.,
+ // "profiles[${i}]" or "my_view_${i}".
+ optional string loop_counter = 1;
+
+ // The value list to loop over.
+ optional string loop_value_model_identifier = 2;
+
+ // The callbacks to invoke for every iteration of the loop.
+ repeated CallbackProto callbacks = 3;
+}
diff --git a/components/autofill_assistant/browser/user_model.cc b/components/autofill_assistant/browser/user_model.cc
index 2130602..85b7daa 100644
--- a/components/autofill_assistant/browser/user_model.cc
+++ b/components/autofill_assistant/browser/user_model.cc
@@ -3,6 +3,7 @@
// found in the LICENSE file.
#include "components/autofill_assistant/browser/user_model.h"
+#include "components/autofill_assistant/browser/field_formatter.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
@@ -55,9 +56,17 @@
return weak_ptr_factory_.GetWeakPtr();
}
-void UserModel::SetValue(const std::string& identifier,
+void UserModel::SetValue(const std::string& input,
const ValueProto& value,
bool force_notification) {
+ // Replace all placeholders in the input.
+ auto formatted_identifier =
+ field_formatter::FormatString(input, identifier_placeholders_);
+ if (!formatted_identifier.has_value()) {
+ VLOG(2) << "Error setting value: placeholder not found for " << input;
+ return;
+ }
+ std::string identifier = *formatted_identifier;
auto result = values_.emplace(identifier, value);
if (!force_notification && !result.second && result.first->second == value &&
value.is_client_side_only() ==
@@ -72,8 +81,14 @@
}
}
-base::Optional<ValueProto> UserModel::GetValue(
- const std::string& identifier) const {
+base::Optional<ValueProto> UserModel::GetValue(const std::string& input) const {
+ // Replace all placeholders in the input.
+ auto formatted_identifier =
+ field_formatter::FormatString(input, identifier_placeholders_);
+ if (!formatted_identifier.has_value()) {
+ return base::nullopt;
+ }
+ std::string identifier = *formatted_identifier;
auto it = values_.find(identifier);
if (it != values_.end()) {
return it->second;
@@ -145,6 +160,20 @@
observers_.RemoveObserver(observer);
}
+void UserModel::AddIdentifierPlaceholders(
+ const std::map<std::string, std::string> placeholders) {
+ for (const auto& placeholder : placeholders) {
+ identifier_placeholders_[placeholder.first] = placeholder.second;
+ }
+}
+
+void UserModel::RemoveIdentifierPlaceholders(
+ const std::map<std::string, std::string> placeholders) {
+ for (const auto& placeholder : placeholders) {
+ identifier_placeholders_.erase(placeholder.first);
+ }
+}
+
void UserModel::SetAutofillCreditCards(
std::unique_ptr<std::vector<std::unique_ptr<autofill::CreditCard>>>
credit_cards) {
diff --git a/components/autofill_assistant/browser/user_model.h b/components/autofill_assistant/browser/user_model.h
index e0b4189..266dee6 100644
--- a/components/autofill_assistant/browser/user_model.h
+++ b/components/autofill_assistant/browser/user_model.h
@@ -48,8 +48,10 @@
bool force_notification = false);
// Returns the value for |identifier| or nullopt if there is no such value.
- // Also supports the array operator to retrieve a specific element of a list,
- // e.g., "identifier[0]" to get the first item.
+ // - Placeholders in |identifier| of the form ${key} are automatically
+ // replaced (see |AddIdentifierPlaceholders|).
+ // - Also supports the array operator to retrieve
+ // a specific element of a list, e.g., "identifier[0]" to get the first item.
base::Optional<ValueProto> GetValue(const std::string& identifier) const;
// Returns the value for |reference| or nullopt if there is no such value.
@@ -72,6 +74,17 @@
return values;
}
+ // Adds a set of placeholders (overwrite if necessary). When looking up values
+ // by identifier, all occurrences of ${key} are automatically replaced by
+ // their value. Example: the current set of placeholders contains "i" -> "1".
+ // Looking up the value "value[${i}]" will now actually lookup "value[1]".
+ void AddIdentifierPlaceholders(
+ const std::map<std::string, std::string> placeholders);
+
+ // Removes a set of placeholders.
+ void RemoveIdentifierPlaceholders(
+ const std::map<std::string, std::string> placeholders);
+
// Replaces the set of available autofill credit cards.
void SetAutofillCreditCards(
std::unique_ptr<std::vector<std::unique_ptr<autofill::CreditCard>>>
@@ -105,6 +118,7 @@
friend class UserModelTest;
std::map<std::string, ValueProto> values_;
+ std::map<std::string, std::string> identifier_placeholders_;
std::map<std::string, std::unique_ptr<autofill::CreditCard>> credit_cards_;
std::map<std::string, std::unique_ptr<autofill::AutofillProfile>> profiles_;
base::ObserverList<Observer> observers_;
diff --git a/components/autofill_assistant/browser/user_model_unittest.cc b/components/autofill_assistant/browser/user_model_unittest.cc
index 799d606..a1d7729 100644
--- a/components/autofill_assistant/browser/user_model_unittest.cc
+++ b/components/autofill_assistant/browser/user_model_unittest.cc
@@ -366,4 +366,75 @@
EXPECT_TRUE(GetValues().at("identifier").is_client_side_only());
}
+TEST_F(UserModelTest, GetValueWithPlaceholders) {
+ ValueProto value;
+ value.mutable_strings()->add_values("a");
+ value.mutable_strings()->add_values("b");
+ value.mutable_strings()->add_values("c");
+ model_.SetValue("multi_value", value);
+ model_.SetValue("single_value_0", SimpleValue(std::string("d")));
+ model_.SetValue("single_value_1", SimpleValue(std::string("e")));
+ model_.SetValue("single_value_2", SimpleValue(std::string("f")));
+
+ EXPECT_EQ(model_.GetValue("multi_value[${i}]"), base::nullopt);
+ EXPECT_EQ(model_.GetValue("single_value_i"), base::nullopt);
+ model_.AddIdentifierPlaceholders({{"i", "0"}});
+ EXPECT_EQ(model_.GetValue("multi_value[${i}]"),
+ SimpleValue(std::string("a")));
+ EXPECT_EQ(model_.GetValue("single_value_${i}"),
+ SimpleValue(std::string("d")));
+
+ // Add placeholder.
+ model_.AddIdentifierPlaceholders({{"j", "1"}});
+ EXPECT_EQ(model_.GetValue("multi_value[${j}]"),
+ SimpleValue(std::string("b")));
+ EXPECT_EQ(model_.GetValue("single_value_${j}"),
+ SimpleValue(std::string("e")));
+ EXPECT_EQ(model_.GetValue("single_value_${j}[${i}]"),
+ SimpleValue(std::string("e")));
+
+ // Overwrite placeholder.
+ model_.AddIdentifierPlaceholders({{"i", "2"}});
+ EXPECT_EQ(model_.GetValue("multi_value[${i}]"),
+ SimpleValue(std::string("c")));
+ EXPECT_EQ(model_.GetValue("single_value_${i}"),
+ SimpleValue(std::string("f")));
+ EXPECT_EQ(model_.GetValue("single_value_${j}[${i}]"), base::nullopt);
+ // Remove placeholder (the value does not matter, it's just about the key).
+ model_.RemoveIdentifierPlaceholders({{"i", "123"}});
+ EXPECT_EQ(model_.GetValue("multi_value[${i}]"), base::nullopt);
+ EXPECT_EQ(model_.GetValue("single_value_${i}"), base::nullopt);
+ EXPECT_EQ(model_.GetValue("single_value_${j}"),
+ SimpleValue(std::string("e")));
+}
+
+TEST_F(UserModelTest, SetValueWithPlaceholders) {
+ ValueProto value;
+ value.mutable_strings()->add_values("a");
+ value.mutable_strings()->add_values("b");
+ value.mutable_strings()->add_values("c");
+ model_.SetValue("value_${i}", value);
+ EXPECT_EQ(model_.GetValue("value_${i}"), base::nullopt);
+
+ model_.AddIdentifierPlaceholders({{"i", "0"}});
+ model_.SetValue("value_${i}", value);
+ EXPECT_EQ(model_.GetValue("value_0"), value);
+ EXPECT_EQ(model_.GetValue("value_${i}"), value);
+
+ model_.RemoveIdentifierPlaceholders({{"i", "0"}});
+ EXPECT_EQ(model_.GetValue("value_0"), value);
+ EXPECT_EQ(model_.GetValue("value_${i}"), base::nullopt);
+
+ model_.AddIdentifierPlaceholders({{"i", "0"}});
+ model_.AddIdentifierPlaceholders({{"j", "1"}});
+ model_.SetValue("value_${i}_${j}", value);
+ EXPECT_EQ(model_.GetValue("value_0_1"), value);
+ EXPECT_EQ(model_.GetValue("value_${i}_${j}"), value);
+
+ model_.RemoveIdentifierPlaceholders({{"j", "1"}});
+ EXPECT_EQ(model_.GetValue("value_${i}_${j}"), base::nullopt);
+ model_.SetValue("value_${i}", value);
+ EXPECT_EQ(model_.GetValue("value_${i}"), value);
+}
+
} // namespace autofill_assistant