Chromium Code Reviews| Index: ios/chrome/browser/passwords/credential_manager_js_unittest.mm |
| diff --git a/ios/chrome/browser/passwords/credential_manager_js_unittest.mm b/ios/chrome/browser/passwords/credential_manager_js_unittest.mm |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..4a6e9144bd6e5a751fb74bd0e534b8b9d421a559 |
| --- /dev/null |
| +++ b/ios/chrome/browser/passwords/credential_manager_js_unittest.mm |
| @@ -0,0 +1,510 @@ |
| +// Copyright 2015 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +#include <memory> |
| + |
| +#include "base/mac/foundation_util.h" |
| +#include "base/mac/scoped_nsobject.h" |
|
Eugene But (OOO till 7-30)
2016/07/13 22:30:31
s/include/import
vabr (Chromium)
2016/07/14 07:36:31
Done.
|
| +#include "base/strings/utf_string_conversions.h" |
| +#include "base/values.h" |
| +#import "ios/chrome/browser/passwords/js_credential_manager.h" |
| +#include "ios/web/public/web_state/credential.h" |
| +#import "ios/web/public/web_state/js/crw_js_injection_receiver.h" |
| +#import "ios/web/public/web_state/web_state.h" |
| +#include "ios/web/public/web_state/web_state_observer.h" |
| +#import "ios/web/public/test/web_test_with_web_state.h" |
| +#include "testing/gmock/include/gmock/gmock.h" |
| +#include "testing/gtest/include/gtest/gtest.h" |
| +#include "testing/gtest_mac.h" |
| +#include "url/gurl.h" |
| + |
| +namespace { |
| + |
| +using ::testing::_; |
| + |
| +// Matcher to match web::Credential. |
| +MATCHER_P(IsEqualTo, value, "") { |
| + return arg.type == value.type && arg.id == value.id && |
| + arg.name == value.name && arg.avatar_url == value.avatar_url && |
| + arg.password == value.password && |
| + arg.federation_origin.Serialize() == |
| + value.federation_origin.Serialize(); |
| +} |
| + |
| +// A mock WebStateObserver for testing the Credential Manager API. |
| +class MockWebStateObserver : public web::WebStateObserver { |
| + public: |
| + explicit MockWebStateObserver(web::WebState* web_state) |
| + : web::WebStateObserver(web_state) {} |
| + ~MockWebStateObserver() override {} |
| + |
| + MOCK_METHOD5( |
| + CredentialsRequested, |
| + void(int, const GURL&, bool, const std::vector<std::string>&, bool)); |
| + MOCK_METHOD3(SignedIn, void(int, const GURL&, const web::Credential&)); |
| + MOCK_METHOD2(SignedIn, void(int, const GURL&)); |
| + MOCK_METHOD2(SignedOut, void(int, const GURL&)); |
| + MOCK_METHOD3(SignInFailed, void(int, const GURL&, const web::Credential&)); |
| + MOCK_METHOD2(SignInFailed, void(int, const GURL&)); |
| + |
| + private: |
| + DISALLOW_COPY_AND_ASSIGN(MockWebStateObserver); |
| +}; |
| + |
| +// Unit tests for the Credential Manager JavaScript and associated plumbing. |
| +class CredentialManagerJsTest : public web::WebTestWithWebState { |
| + public: |
| + CredentialManagerJsTest() {} |
| + |
| + void SetUp() override { |
| + web::WebTestWithWebState::SetUp(); |
| + js_credential_manager_.reset(base::mac::ObjCCastStrict<JSCredentialManager>( |
| + [[web_state()->GetJSInjectionReceiver() |
| + instanceOfClass:[JSCredentialManager class]] retain])); |
| + observer_.reset(new MockWebStateObserver(web_state())); |
| + } |
| + |
| + // Sets up a web page and injects the JSCredentialManager. Must be called |
| + // before any interaction with the page. |
| + void Inject() { |
| + LoadHtml(@""); |
| + [js_credential_manager_ inject]; |
| + } |
| + |
| + // Returns the mock observer. |
| + MockWebStateObserver& observer() { return *observer_; } |
| + |
| + // Returns a string that creates a Credential object for JavaScript testing. |
| + NSString* test_credential_js() { |
| + return @"new PasswordCredential('bob', 'bobiscool', 'Bob Boblaw'," |
| + @"'https://bobboblawslawblog.com/bob.jpg')"; |
| + } |
| + |
| + // Returns a Credential to match the one returned by |test_credential_js()|. |
| + web::Credential test_credential() { |
| + web::Credential test_credential; |
| + test_credential.type = web::CredentialType::CREDENTIAL_TYPE_PASSWORD; |
| + test_credential.id = base::ASCIIToUTF16("bob"); |
| + test_credential.password = base::ASCIIToUTF16("bobiscool"); |
| + test_credential.name = base::ASCIIToUTF16("Bob Boblaw"); |
| + test_credential.avatar_url = GURL("https://bobboblawslawblog.com/bob.jpg"); |
| + return test_credential; |
| + } |
| + |
| + // Adds handlers for resolving and rejecting the promise returned by |
| + // executing the code in |promise|. |
| + void PrepareResolverAndRejecter(NSString* promise) { |
| + EvaluateJavaScriptAsString( |
| + [NSString stringWithFormat:@"var resolved = false; " |
| + @"var rejected = false; " |
| + @"var resolvedCredential = null; " |
| + @"var rejectedError = null; " |
| + @"function resolve(credential) { " |
| + @" resolved = true; " |
| + @" resolvedCredential = credential;" |
| + @"} " |
| + @"function reject(error) { " |
| + @" rejected = true; " |
| + @" rejectedError = error; " |
| + @"} " |
| + @"%@.then(resolve, reject); ", |
| + promise]); |
| + // Wait until the promise executor has executed. |
| + WaitForCondition(^bool { |
| + return [EvaluateJavaScriptAsString( |
| + @"Object.keys(__gCrWeb.credentialManager.resolvers_).length > 0") |
| + isEqualToString:@"true"]; |
| + }); |
| + } |
| + |
| + // Checks that the Credential returned to the resolve handler in JavaScript |
| + // matches the structure of |test_credential()|. |
| + void CheckResolvedCredentialMatchesTestCredential() { |
| + EXPECT_NSEQ(@"true", EvaluateJavaScriptAsString(@"resolved")); |
| + EXPECT_NSEQ( |
| + @"PasswordCredential", |
| + EvaluateJavaScriptAsString(@"resolvedCredential.constructor.name")); |
| + EXPECT_NSEQ(@"bob", EvaluateJavaScriptAsString(@"resolvedCredential.id")); |
| + EXPECT_NSEQ(@"bobiscool", |
| + EvaluateJavaScriptAsString(@"resolvedCredential.password_")); |
| + EXPECT_NSEQ(@"Bob Boblaw", |
| + EvaluateJavaScriptAsString(@"resolvedCredential.name")); |
| + EXPECT_NSEQ(@"https://bobboblawslawblog.com/bob.jpg", |
| + EvaluateJavaScriptAsString(@"resolvedCredential.avatarURL")); |
| + } |
| + |
| + // Checks that the promise set up by |PrepareResolverAndRejecter| was resolved |
| + // without a credential. |
| + void CheckResolvedWithoutCredential() { |
| + EXPECT_NSEQ(@"true", EvaluateJavaScriptAsString(@"resolved")); |
| + EXPECT_NSEQ(@"false", EvaluateJavaScriptAsString(@"!!resolvedCredential")); |
| + } |
| + |
| + // Checks that the promise set up by |PrepareResolverAndRejecter| was rejected |
| + // with an error with name |error_name| and |message|. |
| + void CheckRejected(NSString* error_name, NSString* message) { |
| + EXPECT_NSEQ(@"true", EvaluateJavaScriptAsString(@"rejected")); |
| + EXPECT_NSEQ(error_name, EvaluateJavaScriptAsString(@"rejectedError.name")); |
| + EXPECT_NSEQ(message, EvaluateJavaScriptAsString(@"rejectedError.message")); |
| + } |
| + |
| + // Waits until the promise set up by |PrepareResolverAndRejecter| has been |
| + // either resolved or rejected. |
| + void WaitUntilPromiseResolvedOrRejected() { |
| + WaitForCondition(^bool { |
| + return [EvaluateJavaScriptAsString(@"resolved || rejected") |
| + isEqualToString:@"true"]; |
| + }); |
| + } |
| + |
| + // Resolves the promise set up by |PrepareResolverAndRejecter| and associated |
| + // with |request_id| with |test_credential()|. |
| + void ResolvePromiseWithTestCredential(int request_id) { |
| + __block bool finished = false; |
| + [js_credential_manager() resolvePromiseWithRequestID:request_id |
| + credential:test_credential() |
| + completionHandler:^(BOOL success) { |
| + EXPECT_TRUE(success); |
| + finished = true; |
| + }]; |
| + WaitForCondition(^bool { |
| + return finished; |
| + }); |
| + WaitUntilPromiseResolvedOrRejected(); |
| + } |
| + |
| + // Resolves the promise set up by |PrepareResolverAndRejecter| and associated |
| + // with |request_id| without a credential. |
| + void ResolvePromiseWithoutCredential(int request_id) { |
| + __block bool finished = false; |
| + [js_credential_manager() resolvePromiseWithRequestID:request_id |
| + completionHandler:^(BOOL success) { |
| + EXPECT_TRUE(success); |
| + finished = true; |
| + }]; |
| + WaitForCondition(^bool { |
| + return finished; |
| + }); |
| + WaitUntilPromiseResolvedOrRejected(); |
| + } |
| + |
| + // Rejects the promise set up by |PrepareResolverAndRejecter| and associated |
| + // with |request_id| with an error of type |error_type| and |message|. |
| + void RejectPromise(int request_id, NSString* error_type, NSString* message) { |
| + __block bool finished = false; |
| + [js_credential_manager() rejectPromiseWithRequestID:request_id |
| + errorType:error_type |
| + message:message |
| + completionHandler:^(BOOL success) { |
| + EXPECT_TRUE(success); |
| + finished = true; |
| + }]; |
| + WaitForCondition(^bool { |
| + return finished; |
| + }); |
| + WaitUntilPromiseResolvedOrRejected(); |
| + } |
| + |
| + // Tests that the promise set up by |PrepareResolverAndRejecter| wasn't |
| + // rejected. |
| + void CheckNeverRejected() { |
| + EXPECT_NSEQ(@"false", EvaluateJavaScriptAsString(@"rejected")); |
| + } |
| + |
| + // Tests that the promise set up by |PrepareResolverAndRejecter| wasn't |
| + // resolved. |
| + void CheckNeverResolved() { |
| + EXPECT_NSEQ(@"false", EvaluateJavaScriptAsString(@"resolved")); |
| + } |
| + |
| + // Returns the JSCredentialManager for testing. |
| + JSCredentialManager* js_credential_manager() { |
| + return js_credential_manager_; |
| + } |
| + |
| + // Tests that resolving the promise returned by |promise| and associated with |
| + // |request_id| with |test_credential()| correctly forwards that credential |
| + // to the client. |
| + void TestPromiseResolutionWithCredential(int request_id, NSString* promise) { |
| + PrepareResolverAndRejecter(promise); |
| + ResolvePromiseWithTestCredential(request_id); |
| + CheckResolvedCredentialMatchesTestCredential(); |
| + CheckNeverRejected(); |
| + } |
| + |
| + // Tests that resolving the promise returned by |promise| and associated with |
| + // |request_id| without a credential correctly invokes the client. |
| + void TestPromiseResolutionWithoutCredential(int request_id, |
| + NSString* promise) { |
| + PrepareResolverAndRejecter(promise); |
| + ResolvePromiseWithoutCredential(request_id); |
| + CheckResolvedWithoutCredential(); |
| + CheckNeverRejected(); |
| + } |
| + |
| + // Tests that rejecting the promise returned by |promise| and associated with |
| + // |request_id| with an error of type |error| and message |message| correctly |
| + // forwards that error to the client. |
| + void TestPromiseRejection(int request_id, |
| + NSString* error, |
| + NSString* message, |
| + NSString* promise) { |
| + PrepareResolverAndRejecter(promise); |
| + RejectPromise(request_id, error, message); |
| + CheckRejected(error, message); |
| + CheckNeverResolved(); |
| + } |
| + |
| + private: |
| + // Manager for injected credential manager JavaScript. |
| + base::scoped_nsobject<JSCredentialManager> js_credential_manager_; |
| + |
| + // Mock observer for testing. |
| + std::unique_ptr<MockWebStateObserver> observer_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(CredentialManagerJsTest); |
| +}; |
| + |
| +// Tests that navigator.credentials calls use distinct request identifiers. |
| +TEST_F(CredentialManagerJsTest, RequestIdentifiersDiffer) { |
| + Inject(); |
| + EXPECT_CALL(observer(), CredentialsRequested(0, _, _, _, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.request()"); |
| + EXPECT_CALL(observer(), SignInFailed(1, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.notifyFailedSignIn()"); |
| + EXPECT_CALL(observer(), SignInFailed(2, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.notifyFailedSignIn()"); |
| + EXPECT_CALL(observer(), SignedIn(3, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.notifySignedIn()"); |
| + EXPECT_CALL(observer(), SignedOut(4, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.notifySignedOut()"); |
| + EXPECT_CALL(observer(), CredentialsRequested(5, _, _, _, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.request()"); |
| +} |
| + |
| +// Tests that navigator.credentials.request() creates and forwards the right |
| +// arguments to the app side. |
| +// TODO(rohitrao): Fails after merge r376674. https://crbug.com/588706. |
| +TEST_F(CredentialManagerJsTest, DISABLED_RequestToApp) { |
| + Inject(); |
| + std::vector<std::string> empty_federations; |
| + std::vector<std::string> nonempty_federations; |
| + nonempty_federations.push_back("foo"); |
| + nonempty_federations.push_back("bar"); |
| + |
| + EXPECT_CALL(observer(), |
| + CredentialsRequested(0, _, false, empty_federations, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.request()"); |
| + |
| + EXPECT_CALL(observer(), |
| + CredentialsRequested(1, _, false, empty_federations, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.request({})"); |
| + |
| + EXPECT_CALL(observer(), |
| + CredentialsRequested(2, _, true, empty_federations, _)); |
| + EvaluateJavaScriptAsString( |
| + @"navigator.credentials.request({suppressUI: true})"); |
| + |
| + EXPECT_CALL(observer(), |
| + CredentialsRequested(3, _, false, nonempty_federations, _)); |
| + EvaluateJavaScriptAsString( |
| + @"navigator.credentials.request({federations: ['foo', 'bar']})"); |
| + |
| + EXPECT_CALL(observer(), |
| + CredentialsRequested(4, _, true, nonempty_federations, _)); |
| + EvaluateJavaScriptAsString( |
| + @"navigator.credentials.request(" |
| + @" { suppressUI: true, federations: ['foo', 'bar'] })"); |
| + |
| + EXPECT_CALL(observer(), |
| + CredentialsRequested(5, _, false, empty_federations, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.request(" |
| + @" { suppressUI: false, federations: [] })"); |
| +} |
| + |
| +// Tests that navigator.credentials.notifySignedIn() creates and forwards the |
| +// right arguments to the app side. |
| +TEST_F(CredentialManagerJsTest, NotifySignedInToApp) { |
| + Inject(); |
| + EXPECT_CALL(observer(), SignedIn(0, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.notifySignedIn()"); |
| + |
| + EXPECT_CALL(observer(), SignedIn(1, _, IsEqualTo(test_credential()))); |
| + EvaluateJavaScriptAsString( |
| + [NSString stringWithFormat:@"navigator.credentials.notifySignedIn(%@)", |
| + test_credential_js()]); |
| +} |
| + |
| +// Tests that navigator.credentials.notifySignedOut() creates and forwards the |
| +// right arguments to the app side. |
| +TEST_F(CredentialManagerJsTest, NotifySignedOutToApp) { |
| + Inject(); |
| + EXPECT_CALL(observer(), SignedOut(0, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.notifySignedOut()"); |
| +} |
| + |
| +// Tests that navigator.credentials.notifyFailedSignIn() creates and forwards |
| +// the right arguments to the app side. |
| +TEST_F(CredentialManagerJsTest, NotifyFailedSignInToApp) { |
| + Inject(); |
| + EXPECT_CALL(observer(), SignInFailed(0, _)); |
| + EvaluateJavaScriptAsString(@"navigator.credentials.notifyFailedSignIn()"); |
| + |
| + EXPECT_CALL(observer(), SignInFailed(1, _, IsEqualTo(test_credential()))); |
| + EvaluateJavaScriptAsString([NSString |
| + stringWithFormat:@"navigator.credentials.notifyFailedSignIn(%@)", |
| + test_credential_js()]); |
| +} |
| + |
| +// Tests that resolving the promise returned by a call to |
| +// navigator.credentials.request() with a credential correctly forwards that |
| +// credential to the client. |
| +TEST_F(CredentialManagerJsTest, ResolveRequestPromiseWithCredential) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), CredentialsRequested(request_id, _, _, _, _)); |
| + TestPromiseResolutionWithCredential(request_id, |
| + @"navigator.credentials.request()"); |
| +} |
| + |
| +// Tests that resolving the promise returned by a call to |
| +// navigator.credentials.request() without a credential correctly invokes the |
| +// client handler. |
| +TEST_F(CredentialManagerJsTest, ResolveRequestPromiseWithoutCredential) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), CredentialsRequested(request_id, _, _, _, _)); |
| + TestPromiseResolutionWithoutCredential(request_id, |
| + @"navigator.credentials.request()"); |
| +} |
| + |
| +// Tests that resolving the promise returned by a call to |
| +// navigator.credentials.notifySignedIn() without a credential correctly invokes |
| +// the client handler. |
| +TEST_F(CredentialManagerJsTest, ResolveNotifySignedInPromiseWithoutCredential) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), SignedIn(request_id, _)); |
| + TestPromiseResolutionWithoutCredential( |
| + request_id, @"navigator.credentials.notifySignedIn()"); |
| +} |
| + |
| +// Tests that resolving the promise returned by a call to |
| +// navigator.credentials.notifyFailedSignIn() without a credential correctly |
| +// invokes the client handler. |
| +TEST_F(CredentialManagerJsTest, |
| + ResolveNotifyFailedSignInPromiseWithoutCredential) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), SignInFailed(request_id, _)); |
| + TestPromiseResolutionWithoutCredential( |
| + request_id, @"navigator.credentials.notifyFailedSignIn()"); |
| +} |
| + |
| +// Tests that resolving the promise returned by a call to |
| +// navigator.credentials.notifyFailedSignIn() without a credential correctly |
| +// invokes the client handler. |
| +TEST_F(CredentialManagerJsTest, |
| + ResolveNotifySignedOutPromiseWithoutCredential) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), SignedOut(request_id, _)); |
| + TestPromiseResolutionWithoutCredential( |
| + request_id, @"navigator.credentials.notifySignedOut()"); |
| +} |
| + |
| +// Tests that rejecting the promise returned by a call to |
| +// navigator.credentials.request() with a InvalidStateError correctly forwards |
| +// that error to the client. |
| +TEST_F(CredentialManagerJsTest, RejectRequestPromiseWithInvalidStateError) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), CredentialsRequested(request_id, _, _, _, _)); |
| + TestPromiseRejection(request_id, @"InvalidStateError", @"foo", |
| + @"navigator.credentials.request()"); |
| +} |
| + |
| +// Tests that rejecting the promise returned by a call to |
| +// navigator.credentials.notifySignedIn() with a InvalidStateError correctly |
| +// forwards that error to the client. |
| +TEST_F(CredentialManagerJsTest, |
| + RejectNotifySignedInPromiseWithInvalidStateError) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), SignedIn(request_id, _)); |
| + TestPromiseRejection(request_id, @"InvalidStateError", @"foo", |
| + @"navigator.credentials.notifySignedIn()"); |
| +} |
| + |
| +// Tests that rejecting the promise returned by a call to |
| +// navigator.credentials.notifyFailedSignIn() with a InvalidStateError correctly |
| +// forwards that error to the client. |
| +TEST_F(CredentialManagerJsTest, |
| + RejectNotifyFailedSignInPromiseWithInvalidStateError) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), SignInFailed(request_id, _)); |
| + TestPromiseRejection(request_id, @"InvalidStateError", @"foo", |
| + @"navigator.credentials.notifyFailedSignIn()"); |
| +} |
| + |
| +// Tests that rejecting the promise returned by a call to |
| +// navigator.credentials.notifySignedOut() with a InvalidStateError correctly |
| +// forwards that error to the client. |
| +TEST_F(CredentialManagerJsTest, |
| + RejectNotifySignedOutPromiseWithInvalidStateError) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), SignedOut(request_id, _)); |
| + TestPromiseRejection(request_id, @"InvalidStateError", @"foo", |
| + @"navigator.credentials.notifySignedOut()"); |
| +} |
| + |
| +// Tests that rejecting the promise returned by a call to |
| +// navigator.credentials.request() with a SecurityError correctly forwards that |
| +// error to the client. |
| +TEST_F(CredentialManagerJsTest, RejectRequestPromiseWithSecurityError) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), CredentialsRequested(request_id, _, _, _, _)); |
| + TestPromiseRejection(request_id, @"SecurityError", @"foo", |
| + @"navigator.credentials.request()"); |
| +} |
| + |
| +// Tests that rejecting the promise returned by a call to |
| +// navigator.credentials.notifySignedIn() with a SecurityError correctly |
| +// forwards that error to the client. |
| +TEST_F(CredentialManagerJsTest, RejectNotifySignedInPromiseWithSecurityError) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), SignedIn(request_id, _)); |
| + TestPromiseRejection(request_id, @"SecurityError", @"foo", |
| + @"navigator.credentials.notifySignedIn()"); |
| +} |
| + |
| +// Tests that rejecting the promise returned by a call to |
| +// navigator.credentials.notifyFailedSignIn() with a SecurityError correctly |
| +// forwards that error to the client. |
| +TEST_F(CredentialManagerJsTest, |
| + RejectPromiseWithSecurityError_notifyFailedSignIn) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), SignInFailed(request_id, _)); |
| + TestPromiseRejection(request_id, @"SecurityError", @"foo", |
| + @"navigator.credentials.notifyFailedSignIn()"); |
| +} |
| + |
| +// Tests that rejecting the promise returned by a call to |
| +// navigator.credentials.notifySignedOut() with a SecurityError correctly |
| +// forwards that error to the client. |
| +TEST_F(CredentialManagerJsTest, |
| + RejectPromiseWithSecurityError_notifySignedOut) { |
| + Inject(); |
| + const int request_id = 0; |
| + EXPECT_CALL(observer(), SignedOut(request_id, _)); |
| + TestPromiseRejection(request_id, @"SecurityError", @"foo", |
| + @"navigator.credentials.notifySignedOut()"); |
| +} |
| + |
| +} // namespace |