chromium/ios/web_view/test/web_view_autofill_inttest.mm

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <ChromeWebView/ChromeWebView.h>
#import <Foundation/Foundation.h>

#include "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "components/variations/variations_ids_provider.h"
#import "ios/web_view/public/cwv_navigation_delegate.h"
#import "ios/web_view/test/web_view_inttest_base.h"
#import "ios/web_view/test/web_view_test_util.h"
#import "net/base/apple/url_conversions.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest_mac.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#include "url/gurl.h"

using base::test::ios::kWaitForActionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;
using base::test::ios::kWaitForPageLoadTimeout;

// A stub object that observes the |webViewDidFinishNavigation| event of
// CWVNavigationDelegate. CWVNavigationDelegate is also used as navigation
// policy decider, so OCMProtocolMock doesn't work here because it implements
// all protocol methods which will return NO and block the navigation.
@interface CWVNavigationPageLoadObserver : NSObject <CWVNavigationDelegate>

// Whether |webViewDidFinishNavigation| has been called. Initiated as NO.
@property(nonatomic, assign, readonly) BOOL pageLoaded;

- (void)webViewDidFinishNavigation:(CWVWebView*)webView;

@end

@implementation CWVNavigationPageLoadObserver

- (void)webViewDidFinishNavigation:(CWVWebView*)webView {
  _pageLoaded = YES;
}

@end

namespace ios_web_view {

namespace {
NSString* const kTestFormName = @"FormName";
NSString* const kTestFormID = @"FormID";
NSString* const kTestNameFieldID = @"nameID";
NSString* const kTestAddressFieldID = @"addressID";
NSString* const kTestCityFieldID = @"cityID";
NSString* const kTestStateFieldID = @"stateID";
NSString* const kTestZipFieldID = @"zipID";
NSString* const kTestFieldType = @"text";
NSString* const kTestAddressFieldValue = @"123 Main Street";
NSString* const kTestCityFieldValue = @"Springfield";
NSString* const kTestNameFieldValue = @"Homer Simpson";
NSString* const kTestStateFieldValue = @"IL";
NSString* const kTestZipFieldValue = @"55123";
NSString* const kTestSubmitID = @"SubmitID";
NSString* const kTestFormHtml =
    [NSString stringWithFormat:
                  // Direct form to about:blank to avoid unnecessary navigation.
                  @"<form action='about:blank' name='%@' id='%@'>"
                   "Name <input type='text' name='name' id='%@'>"
                   "Address <input type='text' name='address' id='%@'>"
                   "City <input type='text' name='city' id='%@'>"
                   "State <input type='text' name='state' id='%@'>"
                   "Zip <input type='text' name='zip' id='%@'>"
                   "<input type='submit' id='%@'/>"
                   "</form>",
                  kTestFormName,
                  kTestFormID,
                  kTestNameFieldID,
                  kTestAddressFieldID,
                  kTestCityFieldID,
                  kTestStateFieldID,
                  kTestZipFieldID,
                  kTestSubmitID];
}  // namespace

// Tests autofill features in CWVWebViews.
class WebViewAutofillTest : public WebViewInttestBase {
 protected:
  WebViewAutofillTest()
      : autofill_controller_delegate_(
            OCMProtocolMock(@protocol(CWVAutofillControllerDelegate))) {
    data_source_ =
        OCMStrictProtocolMock(@protocol(CWVSyncControllerDataSource));
    OCMStub([data_source_ allKnownIdentities]).andReturn(@[]);
    CWVSyncController.dataSource = data_source_;
    autofill_controller_ = web_view_.autofillController;
    autofill_controller_.delegate = autofill_controller_delegate_;
  }

  void TearDown() override {
    [(id)data_source_ verify];
    [(id)autofill_controller_delegate_ verify];
  }

  // Loads a test page with a single form and waits until Autofill has parsed
  // that form.
  [[nodiscard]] bool LoadTestPage() {
    std::string html = base::SysNSStringToUTF8(kTestFormHtml);
    GURL url = GetUrlForPageWithHtmlBody(html);
    [[autofill_controller_delegate_ expect]
        autofillController:autofill_controller_
              didFindForms:[OCMArg any]
                   frameID:[OCMArg any]];
    if (!test::LoadUrl(web_view_, net::NSURLWithGURL(url))) {
      return false;
    }
    bool frame_appeared =
        WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
          return !!GetMainFrameId();
        });
    if (!frame_appeared) {
      return false;
    }
    [autofill_controller_delegate_
        verifyWithDelay:kWaitForActionTimeout.InSecondsF()];
    return true;
  }

  [[nodiscard]] bool SubmitForm() {
    NSString* submit_script =
        [NSString stringWithFormat:@"document.getElementById('%@').click();",
                                   kTestSubmitID];
    NSError* error = nil;
    test::EvaluateJavaScript(web_view_, submit_script, &error);
    return !error;
  }

  [[nodiscard]] bool SetFormFieldValue(NSString* field_id,
                                       NSString* field_value) {
    NSString* set_value_script = [NSString
        stringWithFormat:@"document.getElementById('%@').value = '%@';",
                         field_id, field_value];
    NSError* error = nil;
    test::EvaluateJavaScript(web_view_, set_value_script, &error);
    return !error;
  }

  NSArray<CWVAutofillSuggestion*>* FetchSuggestions(NSString* main_frame_id) {
    __block bool suggestions_fetched = false;
    __block NSArray<CWVAutofillSuggestion*>* fetched_suggestions = nil;
    [autofill_controller_
        fetchSuggestionsForFormWithName:kTestFormName
                        fieldIdentifier:kTestAddressFieldID
                              fieldType:kTestFieldType
                                frameID:main_frame_id
                      completionHandler:^(
                          NSArray<CWVAutofillSuggestion*>* suggestions) {
                        fetched_suggestions = suggestions;
                        suggestions_fetched = true;
                      }];
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
      return suggestions_fetched;
    }));
    return fetched_suggestions;
  }

  NSString* GetMainFrameId() {
    NSString* main_frame_id_script = @"__gCrWeb.message.getFrameId();";
    return test::EvaluateJavaScript(web_view_, main_frame_id_script);
  }

  bool WaitUntilPageLoaded() {
    CWVNavigationPageLoadObserver* observer =
        [[CWVNavigationPageLoadObserver alloc] init];
    web_view_.navigationDelegate = observer;
    bool result = WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
      return observer.pageLoaded;
    });
    web_view_.navigationDelegate = nil;
    return result;
  }

  CWVAutofillController* autofill_controller_;
  id autofill_controller_delegate_ = nil;
  id<CWVNavigationDelegate> navigation_delegate_ = nil;
  UIView* dummy_super_view_ = nil;
  id<CWVSyncControllerDataSource> data_source_;
};

// Tests that CWVAutofillControllerDelegate receives callbacks.
TEST_F(WebViewAutofillTest, TestDelegateCallbacks) {
  ASSERT_TRUE(variations::VariationsIdsProvider::GetInstance());
  ASSERT_TRUE(test_server_->Start());
  ASSERT_TRUE(LoadTestPage());
  ASSERT_TRUE(SetFormFieldValue(kTestAddressFieldID, kTestAddressFieldValue));

  [[autofill_controller_delegate_ expect]
                 autofillController:autofill_controller_
      didFocusOnFieldWithIdentifier:kTestAddressFieldID
                          fieldType:kTestFieldType
                           formName:kTestFormName
                            frameID:[OCMArg any]
                              value:kTestAddressFieldValue
                      userInitiated:YES];
  NSString* focus_script =
      [NSString stringWithFormat:@"document.getElementById('%@').focus();",
                                 kTestAddressFieldID];
  NSError* focus_error = nil;
  test::EvaluateJavaScript(web_view_, focus_script, &focus_error);
  ASSERT_FALSE(focus_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];

  [[autofill_controller_delegate_ expect]
                autofillController:autofill_controller_
      didBlurOnFieldWithIdentifier:kTestAddressFieldID
                         fieldType:kTestFieldType
                          formName:kTestFormName
                           frameID:[OCMArg any]
                             value:kTestAddressFieldValue
                     userInitiated:NO];
  NSString* blur_script =
      [NSString stringWithFormat:
                    @"var event = new Event('blur', {bubbles:true});"
                     "document.getElementById('%@').dispatchEvent(event);",
                    kTestAddressFieldID];
  NSError* blur_error = nil;
  test::EvaluateJavaScript(web_view_, blur_script, &blur_error);
  ASSERT_FALSE(blur_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];

  [[autofill_controller_delegate_ expect]
                 autofillController:autofill_controller_
      didInputInFieldWithIdentifier:kTestAddressFieldID
                          fieldType:kTestFieldType
                           formName:kTestFormName
                            frameID:[OCMArg any]
                              value:kTestAddressFieldValue
                      userInitiated:NO];
  // The 'input' event listener defined in form.js is only called during the
  // bubbling phase.
  NSString* input_script =
      [NSString stringWithFormat:
                    @"var event = new Event('input', {'bubbles': true});"
                     "document.getElementById('%@').dispatchEvent(event);",
                    kTestAddressFieldID];
  NSError* input_error = nil;
  test::EvaluateJavaScript(web_view_, input_script, &input_error);
  ASSERT_FALSE(input_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];

  // TODO(crbug.com/40911875): `userInitiated` flipped from `NO` in iOS 16.1 to
  // `YES` in 16.4, so we cannot reliably verify it until the bug is fixed.
  [[[autofill_controller_delegate_ expect] ignoringNonObjectArgs]
         autofillController:autofill_controller_
      didSubmitFormWithName:kTestFormName
                    frameID:[OCMArg any]
              userInitiated:[OCMArg any]];
  // The 'submit' event listener defined in form.js is only called during the
  // bubbling phase.
  NSString* submit_script =
      [NSString stringWithFormat:
                    @"var event = new Event('submit', {'bubbles': true});"
                     "document.getElementById('%@').dispatchEvent(event);",
                    kTestFormID];
  NSError* submit_error = nil;
  test::EvaluateJavaScript(web_view_, submit_script, &submit_error);
  ASSERT_FALSE(submit_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];
}

// Tests that CWVAutofillController can fetch, fill, and clear suggestions.
TEST_F(WebViewAutofillTest, TestSuggestionFetchFillClear) {
  ASSERT_TRUE(test_server_->Start());
  ASSERT_TRUE(LoadTestPage());
  ASSERT_TRUE(SetFormFieldValue(kTestNameFieldID, kTestNameFieldValue));
  ASSERT_TRUE(SetFormFieldValue(kTestAddressFieldID, kTestAddressFieldValue));
  ASSERT_TRUE(SetFormFieldValue(kTestStateFieldID, kTestStateFieldValue));
  ASSERT_TRUE(SetFormFieldValue(kTestCityFieldID, kTestCityFieldValue));
  ASSERT_TRUE(SetFormFieldValue(kTestZipFieldID, kTestZipFieldValue));

  // Stub the confirm save callback to save the new profile right away.
  void (^invocation_handler)(NSInvocation*) = ^(NSInvocation* invocation) {
    void (^decision_handler)(CWVAutofillProfileUserDecision);
    [invocation getArgument:&decision_handler atIndex:5];
    decision_handler(CWVAutofillProfileUserDecisionAccepted);
  };
  [[[autofill_controller_delegate_ stub] andDo:invocation_handler]
                    autofillController:autofill_controller_
      confirmSaveForNewAutofillProfile:[OCMArg any]
                            oldProfile:[OCMArg any]
                       decisionHandler:[OCMArg any]];
  ASSERT_TRUE(SubmitForm());

  // Wait for about:blank to be loaded after <form> submitted.
  ASSERT_TRUE(WaitUntilPageLoaded());

  ASSERT_TRUE(LoadTestPage());

  __block NSString* main_frame_id = nil;

  // The input element needs to be focused before suggestions can be fetched.
  [[autofill_controller_delegate_ expect]
                 autofillController:autofill_controller_
      didFocusOnFieldWithIdentifier:kTestAddressFieldID
                          fieldType:kTestFieldType
                           formName:kTestFormName
                            frameID:[OCMArg checkWithBlock:^BOOL(id frameId) {
                              main_frame_id = frameId;
                              return frameId != nil;
                            }]
                              value:[OCMArg any]
                      userInitiated:YES];
  NSString* focus_script =
      [NSString stringWithFormat:@"document.getElementById('%@').focus()",
                                 kTestAddressFieldID];
  NSError* focus_error = nil;
  test::EvaluateJavaScript(web_view_, focus_script, &focus_error);
  ASSERT_TRUE(!focus_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];

  NSArray<CWVAutofillSuggestion*>* fetched_suggestions =
      FetchSuggestions(main_frame_id);
  ASSERT_EQ(1U, fetched_suggestions.count);
  CWVAutofillSuggestion* fetched_suggestion = fetched_suggestions.firstObject;
  EXPECT_NSEQ(kTestAddressFieldValue, fetched_suggestion.value);
  EXPECT_NSEQ(kTestFormName, fetched_suggestion.formName);
  EXPECT_NSEQ(main_frame_id, fetched_suggestion.frameID);

  [autofill_controller_ acceptSuggestion:fetched_suggestion
                                 atIndex:0
                       completionHandler:nil];
  NSString* filled_script =
      [NSString stringWithFormat:@"document.getElementById('%@').value",
                                 kTestAddressFieldID];
  __block NSError* filled_error = nil;
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
    NSString* filled_value =
        test::EvaluateJavaScript(web_view_, filled_script, &filled_error);
    // If there is an error, early return so the ASSERT catch the error.
    LOG(INFO) << base::SysNSStringToUTF8(filled_value);
    LOG(INFO) << base::SysNSStringToUTF8(fetched_suggestion.value);
    if (filled_error)
      return true;
    return [fetched_suggestion.value isEqualToString:filled_value];
  }));
  ASSERT_FALSE(filled_error);
  [autofill_controller_ clearFormWithName:kTestFormName
                          fieldIdentifier:kTestAddressFieldID
                                  frameID:main_frame_id
                        completionHandler:nil];
  NSString* cleared_script =
      [NSString stringWithFormat:@"document.getElementById('%@').value",
                                 kTestAddressFieldID];
  __block NSError* cleared_error = nil;
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
    NSString* current_value =
        test::EvaluateJavaScript(web_view_, cleared_script, &cleared_error);
    // If there is an error, early return so the ASSERT catch the error.
    if (cleared_error)
      return true;
    return [current_value isEqualToString:@""];
  }));
  EXPECT_FALSE(cleared_error);
}

}  // namespace ios_web_view