| // 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> | 
 |  | 
 | #import "base/strings/sys_string_conversions.h" | 
 | #import "base/test/ios/wait_util.h" | 
 | #import "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" | 
 | #import "net/test/embedded_test_server/embedded_test_server.h" | 
 | #import "testing/gtest_mac.h" | 
 | #import "third_party/ocmock/OCMock/OCMock.h" | 
 | #import "url/gurl.h" | 
 |  | 
 | using base::test::ios::kWaitForActionTimeout; | 
 | using base::test::ios::kWaitForPageLoadTimeout; | 
 | using base::test::ios::WaitUntilConditionOrTimeout; | 
 |  | 
 | // 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.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] | 
 |              perfectFilling:[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 |