diff options
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutocompleteTest.kt')
-rw-r--r-- | mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutocompleteTest.kt | 2532 |
1 files changed, 2532 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutocompleteTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutocompleteTest.kt new file mode 100644 index 0000000000..fbfe2fe46d --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutocompleteTest.kt @@ -0,0 +1,2532 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.geckoview.test + +import android.os.Handler +import android.os.Looper +import android.view.KeyEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.geckoview.Autocomplete.Address +import org.mozilla.geckoview.Autocomplete.AddressSelectOption +import org.mozilla.geckoview.Autocomplete.CreditCard +import org.mozilla.geckoview.Autocomplete.CreditCardSaveOption +import org.mozilla.geckoview.Autocomplete.CreditCardSelectOption +import org.mozilla.geckoview.Autocomplete.LoginEntry +import org.mozilla.geckoview.Autocomplete.LoginSaveOption +import org.mozilla.geckoview.Autocomplete.LoginSelectOption +import org.mozilla.geckoview.Autocomplete.SelectOption +import org.mozilla.geckoview.Autocomplete.StorageDelegate +import org.mozilla.geckoview.Autocomplete.UsedField +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.GeckoSession.PromptDelegate +import org.mozilla.geckoview.GeckoSession.PromptDelegate.AutocompleteRequest +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled + +@RunWith(AndroidJUnit4::class) +@MediumTest +class AutocompleteTest : BaseSessionTest() { + val acceptDelay: Long = 100 + + // This is a utility to delete previous credit card and address information. + // Some credit card tests may not use fetched data since pop up is opened + // before fetching it. + private fun clearData() { + mainSession.loadTestPath(ADDRESS_FORM_HTML_PATH) + mainSession.waitForPageStop() + + val fetchHandled = GeckoResult<Void>() + sessionRule.delegateDuringNextWait(object : StorageDelegate { + override fun onAddressFetch(): GeckoResult<Array<Address>>? { + return null + } + override fun onCreditCardFetch(): GeckoResult<Array<CreditCard>>? { + Handler(Looper.getMainLooper()).postDelayed({ + fetchHandled.complete(null) + }, acceptDelay) + + return null + } + }) + + mainSession.evaluateJS("document.querySelector('#name').focus()") + sessionRule.waitForResult(fetchHandled) + } + + @Test + fun loginBuilderDefaultValue() { + val login = LoginEntry.Builder() + .build() + + assertThat( + "Guid should match", + login.guid, + equalTo(null), + ) + assertThat( + "Origin should match", + login.origin, + equalTo(""), + ) + assertThat( + "Form action origin should match", + login.formActionOrigin, + equalTo(null), + ) + assertThat( + "HTTP realm should match", + login.httpRealm, + equalTo(null), + ) + assertThat( + "Username should match", + login.username, + equalTo(""), + ) + assertThat( + "Password should match", + login.password, + equalTo(""), + ) + } + + @Test + fun fetchLogins() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + ), + ) + + val fetchHandled = GeckoResult<Void>() + + sessionRule.delegateDuringNextWait(object : StorageDelegate { + @AssertCalled(count = 1) + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + assertThat("Domain should match", domain, equalTo("localhost")) + + Handler(Looper.getMainLooper()).postDelayed({ + fetchHandled.complete(null) + }, acceptDelay) + + return null + } + }) + + mainSession.loadTestPath(FORMS3_HTML_PATH) + sessionRule.waitForResult(fetchHandled) + } + + @Test + fun fetchCreditCards() { + val fetchHandled = GeckoResult<Void>() + + mainSession.loadTestPath(CC_FORM_HTML_PATH) + mainSession.waitForPageStop() + + sessionRule.delegateDuringNextWait(object : StorageDelegate { + @AssertCalled(count = 1) + override fun onCreditCardFetch(): GeckoResult<Array<CreditCard>>? { + Handler(Looper.getMainLooper()).postDelayed({ + fetchHandled.complete(null) + }, acceptDelay) + + return null + } + }) + + mainSession.evaluateJS("document.querySelector('#name').focus()") + sessionRule.waitForResult(fetchHandled) + } + + @Test + fun creditCardBuilderDefaultValue() { + val creditCard = CreditCard.Builder() + .build() + + assertThat( + "Guid should match", + creditCard.guid, + equalTo(null), + ) + assertThat( + "Name should match", + creditCard.name, + equalTo(""), + ) + assertThat( + "Number should match", + creditCard.number, + equalTo(""), + ) + assertThat( + "Expiration month should match", + creditCard.expirationMonth, + equalTo(""), + ) + assertThat( + "Expiration year should match", + creditCard.expirationYear, + equalTo(""), + ) + } + + @Test + fun creditCardSelectAndFill() { + // Workaround to fetch and open prompt + clearData() + + // Test: + // 1. Load a credit card form page. + // 2. Focus on the name input field. + // a. Ensure onCreditCardFetch is called. + // b. Return the saved entries. + // c. Ensure onCreditCardSelect is called. + // d. Select and return one of the options. + // e. Ensure the form is filled accordingly. + + val name = arrayOf("Peter Parker", "John Doe") + val number = arrayOf("1234-1234-1234-1234", "2345-2345-2345-2345") + val guid = arrayOf("test-guid1", "test-guid2") + val expMonth = arrayOf("04", "08") + val expYear = arrayOf("22", "23") + val savedCC = arrayOf( + CreditCard.Builder() + .guid(guid[0]) + .name(name[0]) + .number(number[0]) + .expirationMonth(expMonth[0]) + .expirationYear(expYear[0]) + .build(), + CreditCard.Builder() + .guid(guid[1]) + .name(name[1]) + .number(number[1]) + .expirationMonth(expMonth[1]) + .expirationYear(expYear[1]) + .build(), + ) + + val selectHandled = GeckoResult<Void>() + + mainSession.loadTestPath(CC_FORM_HTML_PATH) + mainSession.waitForPageStop() + + sessionRule.delegateDuringNextWait(object : StorageDelegate { + @AssertCalled + override fun onCreditCardFetch(): GeckoResult<Array<CreditCard>>? { + return GeckoResult.fromValue(savedCC) + } + + @AssertCalled(false) + override fun onCreditCardSave(creditCard: CreditCard) {} + }) + + mainSession.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onCreditCardSelect( + session: GeckoSession, + prompt: AutocompleteRequest<CreditCardSelectOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + assertThat( + "There should be two options", + prompt.options.size, + equalTo(2), + ) + + for (i in 0..1) { + val creditCard = prompt.options[i].value + + assertThat("Credit card should not be null", creditCard, notNullValue()) + assertThat( + "Name should match", + creditCard.name, + equalTo(name[i]), + ) + assertThat( + "Number should match", + creditCard.number, + equalTo(number[i]), + ) + assertThat( + "Expiration month should match", + creditCard.expirationMonth, + equalTo(expMonth[i]), + ) + assertThat( + "Expiration year should match", + creditCard.expirationYear, + equalTo(expYear[i]), + ) + } + Handler(Looper.getMainLooper()).postDelayed({ + selectHandled.complete(null) + }, acceptDelay) + + return GeckoResult.fromValue(prompt.confirm(prompt.options[0])) + } + }) + + // Focus on the name input field. + mainSession.evaluateJS("document.querySelector('#name').focus()") + sessionRule.waitForResult(selectHandled) + + assertThat( + "Filled name should match", + mainSession.evaluateJS("document.querySelector('#name').value") as String, + equalTo(name[0]), + ) + assertThat( + "Filled number should match", + mainSession.evaluateJS("document.querySelector('#number').value") as String, + equalTo(number[0]), + ) + assertThat( + "Filled expiration month should match", + mainSession.evaluateJS("document.querySelector('#expMonth').value") as String, + equalTo(expMonth[0]), + ) + assertThat( + "Filled expiration year should match", + mainSession.evaluateJS("document.querySelector('#expYear').value") as String, + equalTo(expYear[0]), + ) + } + + @Test + fun addressBuilderDefaultValue() { + val address = Address.Builder() + .build() + + assertThat( + "Guid should match", + address.guid, + equalTo(null), + ) + assertThat( + "Name should match", + address.name, + equalTo(""), + ) + assertThat( + "Given name should match", + address.givenName, + equalTo(""), + ) + assertThat( + "Family name should match", + address.familyName, + equalTo(""), + ) + assertThat( + "Street address should match", + address.streetAddress, + equalTo(""), + ) + assertThat( + "Address level 1 should match", + address.addressLevel1, + equalTo(""), + ) + assertThat( + "Address level 2 should match", + address.addressLevel2, + equalTo(""), + ) + assertThat( + "Address level 3 should match", + address.addressLevel3, + equalTo(""), + ) + assertThat( + "Postal code should match", + address.postalCode, + equalTo(""), + ) + assertThat( + "Country should match", + address.country, + equalTo(""), + ) + assertThat( + "Tel should match", + address.tel, + equalTo(""), + ) + assertThat( + "Email should match", + address.email, + equalTo(""), + ) + } + + @Test + fun creditCardSelectDismiss() { + // Workaround to fetch and open prompt + clearData() + + val name = arrayOf("Peter Parker", "John Doe", "Taro Yamada") + val number = arrayOf("1234-1234-1234-1234", "2345-2345-2345-2345", "5555-5555-5555-5555") + val guid = arrayOf("test-guid1", "test-guid2", "test-guid3") + val expMonth = arrayOf("04", "08", "12") + val expYear = arrayOf("22", "23", "24") + val savedCC = arrayOf( + CreditCard.Builder() + .guid(guid[0]) + .name(name[0]) + .number(number[0]) + .expirationMonth(expMonth[0]) + .expirationYear(expYear[0]) + .build(), + CreditCard.Builder() + .guid(guid[1]) + .name(name[1]) + .number(number[1]) + .expirationMonth(expMonth[1]) + .expirationYear(expYear[1]) + .build(), + CreditCard.Builder() + .guid(guid[2]) + .name(name[2]) + .number(number[2]) + .expirationMonth(expMonth[2]) + .expirationYear(expYear[2]) + .build(), + ) + + mainSession.loadTestPath(CC_FORM_HTML_PATH) + mainSession.waitForPageStop() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onCreditCardFetch(): GeckoResult<Array<CreditCard>>? { + return GeckoResult.fromValue(savedCC) + } + }) + + val result = GeckoResult<PromptDelegate.PromptResponse>() + val promptInstanceDelegate = object : PromptDelegate.PromptInstanceDelegate { + override fun onPromptDismiss(prompt: PromptDelegate.BasePrompt) { + result.complete(prompt.dismiss()) + } + } + + val promptHandled = GeckoResult<Void>() + mainSession.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled + override fun onCreditCardSelect(session: GeckoSession, prompt: AutocompleteRequest<CreditCardSelectOption>): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat( + "There should be three options", + prompt.options.size, + equalTo(3), + ) + prompt.setDelegate(promptInstanceDelegate) + Handler(Looper.getMainLooper()).postDelayed({ + promptHandled.complete(null) + }, acceptDelay) + + return GeckoResult() + } + }) + + mainSession.evaluateJS("document.querySelector('#name').focus()") + sessionRule.waitForResult(promptHandled) + mainSession.evaluateJS("document.querySelector('#name').blur()") + sessionRule.waitForResult(result) + } + + @Test + fun fetchAddresses() { + val fetchHandled = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled(count = 1) + override fun onAddressFetch(): GeckoResult<Array<Address>>? { + Handler(Looper.getMainLooper()).postDelayed({ + fetchHandled.complete(null) + }, acceptDelay) + + return null + } + }) + + mainSession.loadTestPath(ADDRESS_FORM_HTML_PATH) + mainSession.waitForPageStop() + mainSession.evaluateJS("document.querySelector('#name').focus()") + sessionRule.waitForResult(fetchHandled) + } + + fun checkAddressesForCorrectness(savedAddresses: Array<Address>, selectedAddress: Address) { + // Test: + // 1. Load an address form page. + // 2. Focus on the given name input field. + // a. Ensure onAddressFetch is called. + // b. Return the saved entries. + // c. Ensure onAddressSelect is called. + // d. Select and return one of the options. + // e. Ensure the form is filled accordingly. + + val selectHandled = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onAddressFetch(): GeckoResult<Array<Address>>? { + return GeckoResult.fromValue(savedAddresses) + } + + @AssertCalled(false) + override fun onAddressSave(address: Address) {} + }) + + mainSession.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onAddressSelect( + session: GeckoSession, + prompt: AutocompleteRequest<AddressSelectOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + assertThat( + "There should be one option", + prompt.options.size, + equalTo(savedAddresses.size), + ) + + val addressOption = prompt.options.find { it.value.familyName == selectedAddress.familyName } + val address = addressOption?.value + + assertThat("Address should not be null", address, notNullValue()) + assertThat( + "Guid should match", + address?.guid, + equalTo(selectedAddress.guid), + ) + assertThat( + "Name should match", + address?.name, + equalTo(selectedAddress.name), + ) + assertThat( + "Given name should match", + address?.givenName, + equalTo(selectedAddress.givenName), + ) + assertThat( + "Family name should match", + address?.familyName, + equalTo(selectedAddress.familyName), + ) + assertThat( + "Street address should match", + address?.streetAddress, + equalTo(selectedAddress.streetAddress), + ) + assertThat( + "Address level 1 should match", + address?.addressLevel1, + equalTo(selectedAddress.addressLevel1), + ) + assertThat( + "Address level 2 should match", + address?.addressLevel2, + equalTo(selectedAddress.addressLevel2), + ) + assertThat( + "Address level 3 should match", + address?.addressLevel3, + equalTo(selectedAddress.addressLevel3), + ) + assertThat( + "Postal code should match", + address?.postalCode, + equalTo(selectedAddress.postalCode), + ) + assertThat( + "Country should match", + address?.country, + equalTo(selectedAddress.country), + ) + assertThat( + "Tel should match", + address?.tel, + equalTo(selectedAddress.tel), + ) + assertThat( + "Email should match", + address?.email, + equalTo(selectedAddress.email), + ) + + Handler(Looper.getMainLooper()).postDelayed({ + selectHandled.complete(null) + }, acceptDelay) + + return GeckoResult.fromValue(prompt.confirm(addressOption!!)) + } + }) + + mainSession.loadTestPath(ADDRESS_FORM_HTML_PATH) + mainSession.waitForPageStop() + + // Focus on the given name input field. + mainSession.evaluateJS("document.querySelector('#givenName').focus()") + sessionRule.waitForResult(selectHandled) + + assertThat( + "Filled given name should match", + mainSession.evaluateJS("document.querySelector('#givenName').value") as String, + equalTo(selectedAddress.givenName), + ) + assertThat( + "Filled family name should match", + mainSession.evaluateJS("document.querySelector('#familyName').value") as String, + equalTo(selectedAddress.familyName), + ) + assertThat( + "Filled street address should match", + mainSession.evaluateJS("document.querySelector('#streetAddress').value") as String, + equalTo(selectedAddress.streetAddress), + ) + assertThat( + "Filled country should match", + mainSession.evaluateJS("document.querySelector('#country').value") as String, + equalTo(selectedAddress.country), + ) + assertThat( + "Filled postal code should match", + mainSession.evaluateJS("document.querySelector('#postalCode').value") as String, + equalTo(selectedAddress.postalCode), + ) + assertThat( + "Filled email should match", + mainSession.evaluateJS("document.querySelector('#email').value") as String, + equalTo(selectedAddress.email), + ) + assertThat( + "Filled telephone number should match", + mainSession.evaluateJS("document.querySelector('#tel').value") as String, + equalTo(selectedAddress.tel), + ) + assertThat( + "Filled organization should match", + mainSession.evaluateJS("document.querySelector('#organization').value") as String, + equalTo(selectedAddress.organization), + ) + } + + @Test + fun addressSelectAndFill() { + val name = "Peter Parker" + val givenName = "Peter" + val familyName = "Parker" + val streetAddress = "20 Ingram Street, Forest Hills Gardens, Queens" + val postalCode = "11375" + val country = "US" + val email = "spiderman@newyork.com" + val tel = "+1 180090021" + val organization = "" + val guid = "test-guid" + val savedAddress = Address.Builder() + .guid(guid) + .name(name) + .givenName(givenName) + .familyName(familyName) + .streetAddress(streetAddress) + .postalCode(postalCode) + .country(country) + .email(email) + .tel(tel) + .organization(organization) + .build() + val savedAddresses = mutableListOf<Address>(savedAddress) + + checkAddressesForCorrectness(savedAddresses.toTypedArray(), savedAddress) + } + + @Test + fun addressSelectAndFillMultipleAddresses() { + val names = arrayOf("Peter Parker", "Wade Wilson") + val givenNames = arrayOf("Peter", "Wade") + val familyNames = arrayOf("Parker", "Wilson") + val streetAddresses = arrayOf("20 Ingram Street, Forest Hills Gardens, Queens", "890 Fifth Avenue, Manhattan") + val postalCodes = arrayOf("11375", "10110") + val countries = arrayOf("US", "US") + val emails = arrayOf("spiderman@newyork.com", "deadpool@newyork.com") + val tels = arrayOf("+1 180090021", "+1 180055555") + val organizations = arrayOf("", "") + val guids = arrayOf("test-guid-1", "test-guid-2") + val selectedAddress = Address.Builder() + .guid(guids[1]) + .name(names[1]) + .givenName(givenNames[1]) + .familyName(familyNames[1]) + .streetAddress(streetAddresses[1]) + .postalCode(postalCodes[1]) + .country(countries[1]) + .email(emails[1]) + .tel(tels[1]) + .organization(organizations[1]) + .build() + val savedAddresses = mutableListOf<Address>( + Address.Builder() + .guid(guids[0]) + .name(names[0]) + .givenName(givenNames[0]) + .familyName(familyNames[0]) + .streetAddress(streetAddresses[0]) + .postalCode(postalCodes[0]) + .country(countries[0]) + .email(emails[0]) + .tel(tels[0]) + .organization(organizations[0]) + .build(), + selectedAddress, + ) + + checkAddressesForCorrectness(savedAddresses.toTypedArray(), selectedAddress) + } + + @Test + fun addressSelectDismiss() { + val name = "Peter Parker" + val givenName = "Peter" + val familyName = "Parker" + val streetAddress = "20 Ingram Street, Forest Hills Gardens, Queens" + val postalCode = "11375" + val country = "US" + val email = "spiderman@newyork.com" + val tel = "+1 180090021" + val organization = "" + val guid = "test-guid" + val savedAddress = Address.Builder() + .guid(guid) + .name(name) + .givenName(givenName) + .familyName(familyName) + .streetAddress(streetAddress) + .postalCode(postalCode) + .country(country) + .email(email) + .tel(tel) + .organization(organization) + .build() + val savedAddresses = mutableListOf<Address>(savedAddress) + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onAddressFetch(): GeckoResult<Array<Address>>? { + return GeckoResult.fromValue(savedAddresses.toTypedArray()) + } + }) + + val result = GeckoResult<PromptDelegate.PromptResponse>() + val promptInstanceDelegate = object : PromptDelegate.PromptInstanceDelegate { + override fun onPromptDismiss(prompt: PromptDelegate.BasePrompt) { + result.complete(prompt.dismiss()) + } + } + + val promptHandled = GeckoResult<Void>() + mainSession.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled + override fun onAddressSelect(session: GeckoSession, prompt: AutocompleteRequest<AddressSelectOption>): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat( + "There should be one option", + prompt.options.size, + equalTo(1), + ) + prompt.setDelegate(promptInstanceDelegate) + Handler(Looper.getMainLooper()).postDelayed({ + promptHandled.complete(null) + }, acceptDelay) + + return GeckoResult() + } + }) + + mainSession.loadTestPath(ADDRESS_FORM_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.evaluateJS("document.querySelector('#givenName').focus()") + sessionRule.waitForResult(promptHandled) + mainSession.evaluateJS("document.querySelector('#givenName').blur()") + sessionRule.waitForResult(result) + } + + @Test + fun loginSaveDismiss() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + sessionRule.delegateDuringNextWait(object : StorageDelegate { + @AssertCalled(count = 1) + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + assertThat("Domain should match", domain, equalTo("localhost")) + + return null + } + }) + + mainSession.loadTestPath(FORMS3_HTML_PATH) + mainSession.waitForPageStop() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled(count = 0) + override fun onLoginSave(login: LoginEntry) {} + }) + + // Assign login credentials. + mainSession.evaluateJS("document.querySelector('#user1').value = 'user1x'") + mainSession.evaluateJS("document.querySelector('#pass1').value = 'pass1x'") + + // Submit the form. + mainSession.evaluateJS("document.querySelector('#form1').submit()") + + sessionRule.waitUntilCalled(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + val option = prompt.options[0] + val login = option.value + + assertThat("Session should not be null", session, notNullValue()) + assertThat("Login should not be null", login, notNullValue()) + assertThat( + "Username should match", + login.username, + equalTo("user1x"), + ) + + assertThat( + "Password should match", + login.password, + equalTo("pass1x"), + ) + + return GeckoResult.fromValue(prompt.dismiss()) + } + }) + } + + @Test + fun loginSaveAccept() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + mainSession.loadTestPath(FORMS3_HTML_PATH) + mainSession.waitForPageStop() + + val saveHandled = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginSave(login: LoginEntry) { + assertThat( + "Username should match", + login.username, + equalTo("user1x"), + ) + + assertThat( + "Password should match", + login.password, + equalTo("pass1x"), + ) + + saveHandled.complete(null) + } + }) + + sessionRule.delegateDuringNextWait(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + val option = prompt.options[0] + val login = option.value + + assertThat("Login should not be null", login, notNullValue()) + + assertThat( + "Username should match", + login.username, + equalTo("user1x"), + ) + + assertThat( + "Password should match", + login.password, + equalTo("pass1x"), + ) + + return GeckoResult.fromValue(prompt.confirm(option)) + } + }) + + // Assign login credentials. + mainSession.evaluateJS("document.querySelector('#user1').value = 'user1x'") + mainSession.evaluateJS("document.querySelector('#pass1').value = 'pass1x'") + + // Submit the form. + mainSession.evaluateJS("document.querySelector('#form1').submit()") + + sessionRule.waitForResult(saveHandled) + } + + @Test + fun loginSaveModifyAccept() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + mainSession.loadTestPath(FORMS3_HTML_PATH) + mainSession.waitForPageStop() + + val saveHandled = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginSave(login: LoginEntry) { + assertThat( + "Username should match", + login.username, + equalTo("user1x"), + ) + + assertThat( + "Password should match", + login.password, + equalTo("pass1xmod"), + ) + + saveHandled.complete(null) + } + }) + + sessionRule.delegateDuringNextWait(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + val option = prompt.options[0] + val login = option.value + + assertThat("Login should not be null", login, notNullValue()) + + assertThat( + "Username should match", + login.username, + equalTo("user1x"), + ) + + assertThat( + "Password should match", + login.password, + equalTo("pass1x"), + ) + + val modLogin = LoginEntry.Builder() + .origin(login.origin) + .formActionOrigin(login.origin) + .httpRealm(login.httpRealm) + .username(login.username) + .password("pass1xmod") + .build() + + return GeckoResult.fromValue(prompt.confirm(LoginSaveOption(modLogin))) + } + }) + + // Assign login credentials. + mainSession.evaluateJS("document.querySelector('#user1').value = 'user1x'") + mainSession.evaluateJS("document.querySelector('#pass1').value = 'pass1x'") + + // Submit the form. + mainSession.evaluateJS("document.querySelector('#form1').submit()") + + sessionRule.waitForResult(saveHandled) + } + + @Test + fun loginUpdateAccept() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + val saveHandled = GeckoResult<Void>() + val saveHandled2 = GeckoResult<Void>() + + val user1 = "user1x" + val pass1 = "pass1x" + val pass2 = "pass1up" + val guid = "test-guid" + val savedLogins = mutableListOf<LoginEntry>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + assertThat("Domain should match", domain, equalTo("localhost")) + + return GeckoResult.fromValue(savedLogins.toTypedArray()) + } + + @AssertCalled(count = 2) + override fun onLoginSave(login: LoginEntry) { + assertThat( + "Username should match", + login.username, + equalTo(user1), + ) + + assertThat( + "Password should match", + login.password, + equalTo(forEachCall(pass1, pass2)), + ) + + assertThat( + "GUID should match", + login.guid, + equalTo(forEachCall(null, guid)), + ) + + val savedLogin = LoginEntry.Builder() + .guid(guid) + .origin(login.origin) + .formActionOrigin(login.formActionOrigin) + .username(login.username) + .password(login.password) + .build() + + savedLogins.add(savedLogin) + + if (sessionRule.currentCall.counter == 1) { + saveHandled.complete(null) + } else if (sessionRule.currentCall.counter == 2) { + saveHandled2.complete(null) + } + } + }) + + sessionRule.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled(count = 2) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + val option = prompt.options[0] + val login = option.value + + assertThat("Login should not be null", login, notNullValue()) + + assertThat( + "Username should match", + login.username, + equalTo(user1), + ) + + assertThat( + "Password should match", + login.password, + equalTo(forEachCall(pass1, pass2)), + ) + + return GeckoResult.fromValue(prompt.confirm(option)) + } + }) + + // Assign login credentials. + mainSession.loadTestPath(FORMS3_HTML_PATH) + mainSession.waitForPageStop() + mainSession.evaluateJS("document.querySelector('#user1').value = '$user1'") + mainSession.evaluateJS("document.querySelector('#pass1').value = '$pass1'") + mainSession.evaluateJS("document.querySelector('#form1').submit()") + + sessionRule.waitForResult(saveHandled) + + // Update login credentials. + val session2 = sessionRule.createOpenSession() + session2.loadTestPath(FORMS3_HTML_PATH) + session2.waitForPageStop() + session2.evaluateJS("document.querySelector('#pass1').value = '$pass2'") + session2.evaluateJS("document.querySelector('#form1').submit()") + + sessionRule.waitForResult(saveHandled2) + } + + @Test + fun creditCardSaveAccept() { + val ccName = "MyCard" + val ccNumber = "5105105105105100" + val ccExpMonth = "6" + val ccExpYear = "2024" + + mainSession.loadTestPath(CC_FORM_HTML_PATH) + mainSession.waitForPageStop() + + val saveHandled = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onCreditCardSave(creditCard: CreditCard) { + assertThat("Credit card name should match", creditCard.name, equalTo(ccName)) + assertThat("Credit card number should match", creditCard.number, equalTo(ccNumber)) + assertThat("Credit card expiration month should match", creditCard.expirationMonth, equalTo(ccExpMonth)) + assertThat("Credit card expiration year should match", creditCard.expirationYear, equalTo(ccExpYear)) + saveHandled.complete(null) + } + }) + + sessionRule.delegateDuringNextWait(object : PromptDelegate { + @AssertCalled + override fun onCreditCardSave( + session: GeckoSession, + request: AutocompleteRequest<CreditCardSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse> { + assertThat("Session should not be null", session, notNullValue()) + + val option = request.options[0] + val cc = option.value + + assertThat("Credit card should not be null", cc, notNullValue()) + + assertThat( + "Credit card name should match", + cc.name, + equalTo(ccName), + ) + assertThat( + "Credit card number should match", + cc.number, + equalTo(ccNumber), + ) + assertThat( + "Credit card expiration month should match", + cc.expirationMonth, + equalTo(ccExpMonth), + ) + assertThat( + "Credit card expiration year should match", + cc.expirationYear, + equalTo(ccExpYear), + ) + + return GeckoResult.fromValue(request.confirm(option)) + } + }) + + // Enter the card values + mainSession.evaluateJS("document.querySelector('#name').value = '$ccName'") + mainSession.evaluateJS("document.querySelector('#name').focus()") + mainSession.evaluateJS("document.querySelector('#number').value = '$ccNumber'") + mainSession.evaluateJS("document.querySelector('#number').focus()") + mainSession.evaluateJS("document.querySelector('#expMonth').value = '$ccExpMonth'") + mainSession.evaluateJS("document.querySelector('#expMonth').focus()") + mainSession.evaluateJS("document.querySelector('#expYear').value = '$ccExpYear'") + mainSession.evaluateJS("document.querySelector('#expYear').focus()") + + // Submit the form + mainSession.evaluateJS("document.querySelector('form').requestSubmit()") + + sessionRule.waitForResult(saveHandled) + } + + @Test + fun creditCardSaveAcceptForm2() { + // TODO Bug 1764709: Right now we fill normalized credit card data to match + // the expected result. + val ccName = "MyCard" + val ccNumber = "5105105105105100" + val ccExpMonth = "6" + val ccExpYear = "2024" + + mainSession.loadTestPath(CC_FORM_HTML_PATH) + mainSession.waitForPageStop() + + val saveHandled = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onCreditCardSave(creditCard: CreditCard) { + assertThat("Credit card name should match", creditCard.name, equalTo(ccName)) + assertThat("Credit card number should match", creditCard.number, equalTo(ccNumber)) + assertThat("Credit card expiration month should match", creditCard.expirationMonth, equalTo(ccExpMonth)) + assertThat("Credit card expiration year should match", creditCard.expirationYear, equalTo(ccExpYear)) + saveHandled.complete(null) + } + }) + + sessionRule.delegateDuringNextWait(object : PromptDelegate { + @AssertCalled + override fun onCreditCardSave( + session: GeckoSession, + request: AutocompleteRequest<CreditCardSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse> { + assertThat("Session should not be null", session, notNullValue()) + + val option = request.options[0] + val cc = option.value + + assertThat("Credit card should not be null", cc, notNullValue()) + + assertThat( + "Credit card name should match", + cc.name, + equalTo(ccName), + ) + assertThat( + "Credit card number should match", + cc.number, + equalTo(ccNumber), + ) + assertThat( + "Credit card expiration month should match", + cc.expirationMonth, + equalTo(ccExpMonth), + ) + assertThat( + "Credit card expiration year should match", + cc.expirationYear, + equalTo(ccExpYear), + ) + + return GeckoResult.fromValue(request.confirm(option)) + } + }) + + // Enter the card values + mainSession.evaluateJS("document.querySelector('#form2 #name').value = '$ccName'") + mainSession.evaluateJS("document.querySelector('#form2 #name').focus()") + mainSession.evaluateJS("document.querySelector('#form2 #number').value = '$ccNumber'") + mainSession.evaluateJS("document.querySelector('#form2 #number').focus()") + mainSession.evaluateJS("document.querySelector('#form2 #exp').value = '$ccExpMonth/$ccExpYear'") + mainSession.evaluateJS("document.querySelector('#form2 #exp').focus()") + + // Submit the form + mainSession.evaluateJS("document.querySelector('#form2').requestSubmit()") + + sessionRule.waitForResult(saveHandled) + } + + @Test + fun creditCardSaveDismiss() { + val ccName = "MyCard" + val ccNumber = "5105105105105100" + val ccExpMonth = "6" + val ccExpYear = "2024" + + mainSession.loadTestPath(CC_FORM_HTML_PATH) + mainSession.waitForPageStop() + + sessionRule.delegateDuringNextWait(object : StorageDelegate { + @AssertCalled + override fun onCreditCardFetch(): GeckoResult<Array<CreditCard>>? { + return null + } + }) + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled(count = 0) + override fun onCreditCardSave(creditCard: CreditCard) {} + }) + + // Enter the card values + mainSession.evaluateJS("document.querySelector('#name').value = '$ccName'") + mainSession.evaluateJS("document.querySelector('#name').focus()") + mainSession.evaluateJS("document.querySelector('#number').value = '$ccNumber'") + mainSession.evaluateJS("document.querySelector('#number').focus()") + mainSession.evaluateJS("document.querySelector('#expMonth').value = '$ccExpMonth'") + mainSession.evaluateJS("document.querySelector('#expMonth').focus()") + mainSession.evaluateJS("document.querySelector('#expYear').value = '$ccExpYear'") + mainSession.evaluateJS("document.querySelector('#expYear').focus()") + + // Submit the form + mainSession.evaluateJS("document.querySelector('form').requestSubmit()") + + sessionRule.waitUntilCalled(object : PromptDelegate { + @AssertCalled + override fun onCreditCardSave( + session: GeckoSession, + request: AutocompleteRequest<CreditCardSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse> { + assertThat("Session should not be null", session, notNullValue()) + + val option = request.options[0] + val cc = option.value + + assertThat("Credit card should not be null", cc, notNullValue()) + + assertThat( + "Credit card name should match", + cc.name, + equalTo(ccName), + ) + assertThat( + "Credit card number should match", + cc.number, + equalTo(ccNumber), + ) + assertThat( + "Credit card expiration month should match", + cc.expirationMonth, + equalTo(ccExpMonth), + ) + assertThat( + "Credit card expiration year should match", + cc.expirationYear, + equalTo(ccExpYear), + ) + + return GeckoResult.fromValue(request.dismiss()) + } + }) + } + + @Test + fun creditCardSaveModifyAccept() { + val ccName = "MyCard" + val ccNumber = "5105105105105100" + val ccExpMonth = "6" + val ccExpYearNew = "2026" + val ccExpYear = "2024" + + mainSession.loadTestPath(CC_FORM_HTML_PATH) + mainSession.waitForPageStop() + + val saveHandled = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onCreditCardSave(creditCard: CreditCard) { + assertThat("Credit card name should match", creditCard.name, equalTo(ccName)) + assertThat("Credit card number should match", creditCard.number, equalTo(ccNumber)) + assertThat("Credit card expiration month should match", creditCard.expirationMonth, equalTo(ccExpMonth)) + assertThat("Credit card expiration year should match", creditCard.expirationYear, equalTo(ccExpYearNew)) + saveHandled.complete(null) + } + }) + + sessionRule.delegateDuringNextWait(object : PromptDelegate { + @AssertCalled + override fun onCreditCardSave( + session: GeckoSession, + request: AutocompleteRequest<CreditCardSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse> { + assertThat("Session should not be null", session, notNullValue()) + + val option = request.options[0] + val cc = option.value + + assertThat("Credit card should not be null", cc, notNullValue()) + + assertThat( + "Credit card name should match", + cc.name, + equalTo(ccName), + ) + assertThat( + "Credit card number should match", + cc.number, + equalTo(ccNumber), + ) + assertThat( + "Credit card expiration month should match", + cc.expirationMonth, + equalTo(ccExpMonth), + ) + assertThat( + "Credit card expiration year should match", + cc.expirationYear, + equalTo(ccExpYear), + ) + + val modifiedCreditCard = CreditCard.Builder() + .name(cc.name) + .number(cc.number) + .expirationMonth(cc.expirationMonth) + .expirationYear(ccExpYearNew) + .build() + + return GeckoResult.fromValue(request.confirm(CreditCardSaveOption(modifiedCreditCard))) + } + }) + + // Enter the card values + mainSession.evaluateJS("document.querySelector('#name').value = '$ccName'") + mainSession.evaluateJS("document.querySelector('#name').focus()") + mainSession.evaluateJS("document.querySelector('#number').value = '$ccNumber'") + mainSession.evaluateJS("document.querySelector('#number').focus()") + mainSession.evaluateJS("document.querySelector('#expMonth').value = '$ccExpMonth'") + mainSession.evaluateJS("document.querySelector('#expMonth').focus()") + mainSession.evaluateJS("document.querySelector('#expYear').value = '$ccExpYear'") + mainSession.evaluateJS("document.querySelector('#expYear').focus()") + + // Submit the form + mainSession.evaluateJS("document.querySelector('form').requestSubmit()") + + sessionRule.waitForResult(saveHandled) + } + + @Test + fun creditCardUpdateAccept() { + val ccName = "MyCard" + val ccNumber1 = "5105105105105100" + val ccExpMonth1 = "6" + val ccExpYear1 = "2024" + val ccNumber2 = "4111111111111111" + val ccExpMonth2 = "11" + val ccExpYear2 = "2021" + val savedCreditCards = mutableListOf<CreditCard>() + + mainSession.loadTestPath(CC_FORM_HTML_PATH) + mainSession.waitForPageStop() + + val saveHandled1 = GeckoResult<Void>() + val saveHandled2 = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onCreditCardFetch(): GeckoResult<Array<CreditCard>> { + return GeckoResult.fromValue(savedCreditCards.toTypedArray()) + } + + @AssertCalled(count = 2) + override fun onCreditCardSave(creditCard: CreditCard) { + assertThat( + "Credit card name should match", + creditCard.name, + equalTo(ccName), + ) + assertThat( + "Credit card number should match", + creditCard.number, + equalTo(forEachCall(ccNumber1, ccNumber2)), + ) + assertThat( + "Credit card expiration month should match", + creditCard.expirationMonth, + equalTo(forEachCall(ccExpMonth1, ccExpMonth2)), + ) + assertThat( + "Credit card expiration year should match", + creditCard.expirationYear, + equalTo(forEachCall(ccExpYear1, ccExpYear2)), + ) + + val savedCC = CreditCard.Builder() + .guid("test1") + .name(creditCard.name) + .number(creditCard.number) + .expirationMonth(creditCard.expirationMonth) + .expirationYear(creditCard.expirationYear) + .build() + savedCreditCards.add(savedCC) + + if (sessionRule.currentCall.counter == 1) { + saveHandled1.complete(null) + } else if (sessionRule.currentCall.counter == 2) { + saveHandled2.complete(null) + } + } + }) + + sessionRule.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled(count = 2) + override fun onCreditCardSave( + session: GeckoSession, + request: AutocompleteRequest<CreditCardSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse> { + assertThat("Session should not be null", session, notNullValue()) + + val option = request.options[0] + val cc = option.value + + assertThat("Credit card should not be null", cc, notNullValue()) + + assertThat( + "Credit card name should match", + cc.name, + equalTo(ccName), + ) + assertThat( + "Credit card number should match", + cc.number, + equalTo(forEachCall(ccNumber1, ccNumber2)), + ) + assertThat( + "Credit card expiration month should match", + cc.expirationMonth, + equalTo(forEachCall(ccExpMonth1, ccExpMonth2)), + ) + assertThat( + "Credit card expiration year should match", + cc.expirationYear, + equalTo(forEachCall(ccExpYear1, ccExpYear2)), + ) + + return GeckoResult.fromValue(request.confirm(option)) + } + }) + + // Enter the card values + mainSession.evaluateJS("document.querySelector('#name').value = '$ccName'") + mainSession.evaluateJS("document.querySelector('#name').focus()") + mainSession.evaluateJS("document.querySelector('#number').value = '$ccNumber1'") + mainSession.evaluateJS("document.querySelector('#number').focus()") + mainSession.evaluateJS("document.querySelector('#expMonth').value = '$ccExpMonth1'") + mainSession.evaluateJS("document.querySelector('#expMonth').focus()") + mainSession.evaluateJS("document.querySelector('#expYear').value = '$ccExpYear1'") + mainSession.evaluateJS("document.querySelector('#expYear').focus()") + + // Submit the form + mainSession.evaluateJS("document.querySelector('form').requestSubmit()") + + sessionRule.waitForResult(saveHandled1) + + // Update credit card + val session2 = sessionRule.createOpenSession() + session2.loadTestPath(CC_FORM_HTML_PATH) + session2.waitForPageStop() + session2.evaluateJS("document.querySelector('#name').value = '$ccName'") + session2.evaluateJS("document.querySelector('#name').focus()") + session2.evaluateJS("document.querySelector('#number').value = '$ccNumber2'") + session2.evaluateJS("document.querySelector('#number').focus()") + session2.evaluateJS("document.querySelector('#expMonth').value = '$ccExpMonth2'") + session2.evaluateJS("document.querySelector('#expMonth').focus()") + session2.evaluateJS("document.querySelector('#expYear').value = '$ccExpYear2'") + session2.evaluateJS("document.querySelector('#expYear').focus()") + + session2.evaluateJS("document.querySelector('form').requestSubmit()") + + sessionRule.waitForResult(saveHandled2) + } + + fun testLoginUsed(autofillEnabled: Boolean) { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + val usedHandled = GeckoResult<Void>() + + val user1 = "user1x" + val pass1 = "pass1x" + val guid = "test-guid" + val origin = GeckoSessionTestRule.TEST_ENDPOINT + val savedLogin = LoginEntry.Builder() + .guid(guid) + .origin(origin) + .formActionOrigin(origin) + .username(user1) + .password(pass1) + .build() + val savedLogins = mutableListOf<LoginEntry>(savedLogin) + + if (autofillEnabled) { + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + assertThat("Domain should match", domain, equalTo("localhost")) + + return GeckoResult.fromValue(savedLogins.toTypedArray()) + } + + @AssertCalled(count = 1) + override fun onLoginUsed(login: LoginEntry, usedFields: Int) { + assertThat( + "Used fields should match", + usedFields, + equalTo(UsedField.PASSWORD), + ) + + assertThat( + "Username should match", + login.username, + equalTo(user1), + ) + + assertThat( + "Password should match", + login.password, + equalTo(pass1), + ) + + assertThat( + "GUID should match", + login.guid, + equalTo(guid), + ) + + usedHandled.complete(null) + } + }) + } else { + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + assertThat("Domain should match", domain, equalTo("localhost")) + + return GeckoResult.fromValue(savedLogins.toTypedArray()) + } + + @AssertCalled(false) + override fun onLoginUsed(login: LoginEntry, usedFields: Int) {} + }) + } + + sessionRule.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled(false) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + return null + } + }) + + mainSession.loadTestPath(FORMS3_HTML_PATH) + mainSession.waitForPageStop() + mainSession.evaluateJS("document.querySelector('#form1').submit()") + + if (autofillEnabled) { + sessionRule.waitForResult(usedHandled) + } else { + mainSession.waitForPageStop() + } + } + + @Test + fun loginUsed() { + testLoginUsed(true) + } + + @Test + fun loginAutofillDisabled() { + sessionRule.runtime.settings.loginAutofillEnabled = false + testLoginUsed(false) + sessionRule.runtime.settings.loginAutofillEnabled = true + } + + fun testPasswordAutofill(autofillEnabled: Boolean) { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + val user1 = "user1x" + val pass1 = "pass1x" + val guid = "test-guid" + val origin = GeckoSessionTestRule.TEST_ENDPOINT + val savedLogin = LoginEntry.Builder() + .guid(guid) + .origin(origin) + .formActionOrigin(origin) + .username(user1) + .password(pass1) + .build() + val savedLogins = mutableListOf<LoginEntry>(savedLogin) + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + assertThat("Domain should match", domain, equalTo("localhost")) + + return GeckoResult.fromValue(savedLogins.toTypedArray()) + } + + @AssertCalled(false) + override fun onLoginUsed(login: LoginEntry, usedFields: Int) {} + }) + + sessionRule.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled(false) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + return null + } + }) + + mainSession.loadTestPath(FORMS3_HTML_PATH) + mainSession.waitForPageStop() + mainSession.evaluateJS("document.querySelector('#user1').focus()") + mainSession.evaluateJS( + "document.querySelector('#user1').value = '$user1'", + ) + mainSession.pressKey(KeyEvent.KEYCODE_TAB) + + val pass = mainSession.evaluateJS( + "document.querySelector('#pass1').value", + ) as String + + if (autofillEnabled) { + assertThat( + "Password should match", + pass, + equalTo(pass1), + ) + } else { + assertThat( + "Password should not be filled", + pass, + equalTo(""), + ) + } + } + + @Test + fun loginAutofillDisabledPasswordAutofill() { + sessionRule.runtime.settings.loginAutofillEnabled = false + testPasswordAutofill(false) + sessionRule.runtime.settings.loginAutofillEnabled = true + } + + @Test + fun loginAutofillEnabledPasswordAutofill() { + testPasswordAutofill(true) + } + + @Test + fun loginSelectAccept() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "dom.disable_open_during_load" to false, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + // Test: + // 1. Load a login form page. + // 2. Input un/pw and submit. + // a. Ensure onLoginSave is called accordingly. + // b. Save the submitted login entry. + // 3. Reload the login form page. + // a. Ensure onLoginFetch is called. + // b. Return empty login entry list to avoid autofilling. + // 4. Input a new set of un/pw and submit. + // a. Ensure onLoginSave is called again. + // b. Save the submitted login entry. + // 5. Reload the login form page. + // 6. Focus on the username input field. + // a. Ensure onLoginFetch is called. + // b. Return the saved login entries. + // c. Ensure onLoginSelect is called. + // d. Select and return one of the options. + // e. Submit the form. + // f. Ensure that onLoginUsed is called. + + val user1 = "user1x" + val user2 = "user2x" + val pass1 = "pass1x" + val pass2 = "pass2x" + val savedLogins = mutableListOf<LoginEntry>() + + val saveHandled1 = GeckoResult<Void>() + val saveHandled2 = GeckoResult<Void>() + val selectHandled = GeckoResult<Void>() + val usedHandled = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + assertThat("Domain should match", domain, equalTo("localhost")) + + var logins = mutableListOf<LoginEntry>() + + if (savedLogins.size == 2) { + logins = savedLogins + } + + return GeckoResult.fromValue(logins.toTypedArray()) + } + + @AssertCalled(count = 2) + override fun onLoginSave(login: LoginEntry) { + var username = "" + var password = "" + var handle = GeckoResult<Void>() + + if (sessionRule.currentCall.counter == 1) { + username = user1 + password = pass1 + handle = saveHandled1 + } else if (sessionRule.currentCall.counter == 2) { + username = user2 + password = pass2 + handle = saveHandled2 + } + + val savedLogin = LoginEntry.Builder() + .guid(login.username) + .origin(login.origin) + .formActionOrigin(login.formActionOrigin) + .username(login.username) + .password(login.password) + .build() + + savedLogins.add(savedLogin) + + assertThat( + "Username should match", + login.username, + equalTo(username), + ) + + assertThat( + "Password should match", + login.password, + equalTo(password), + ) + + handle.complete(null) + } + + @AssertCalled(count = 1) + override fun onLoginUsed(login: LoginEntry, usedFields: Int) { + assertThat( + "Used fields should match", + usedFields, + equalTo(UsedField.PASSWORD), + ) + + assertThat( + "Username should match", + login.username, + equalTo(user1), + ) + + assertThat( + "Password should match", + login.password, + equalTo(pass1), + ) + + assertThat( + "GUID should match", + login.guid, + equalTo(user1), + ) + + usedHandled.complete(null) + } + }) + + mainSession.loadTestPath(FORMS3_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.delegateDuringNextWait(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + val option = prompt.options[0] + val login = option.value + + assertThat("Login should not be null", login, notNullValue()) + + assertThat( + "Username should match", + login.username, + equalTo(user1), + ) + + assertThat( + "Password should match", + login.password, + equalTo(pass1), + ) + + return GeckoResult.fromValue(prompt.confirm(option)) + } + }) + + // Assign login credentials. + mainSession.evaluateJS("document.querySelector('#user1').value = '$user1'") + mainSession.evaluateJS("document.querySelector('#pass1').value = '$pass1'") + + // Submit the form. + mainSession.evaluateJS("document.querySelector('#form1').submit()") + sessionRule.waitForResult(saveHandled1) + + // Reload. + val session2 = sessionRule.createOpenSession() + session2.loadTestPath(FORMS3_HTML_PATH) + session2.waitForPageStop() + + session2.delegateDuringNextWait(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + val option = prompt.options[0] + val login = option.value + + assertThat("Login should not be null", login, notNullValue()) + + assertThat( + "Username should match", + login.username, + equalTo(user2), + ) + + assertThat( + "Password should match", + login.password, + equalTo(pass2), + ) + + return GeckoResult.fromValue(prompt.confirm(option)) + } + }) + + // Assign alternative login credentials. + session2.evaluateJS("document.querySelector('#user1').value = '$user2'") + session2.evaluateJS("document.querySelector('#pass1').value = '$pass2'") + + // Submit the form. + session2.evaluateJS("document.querySelector('#form1').submit()") + sessionRule.waitForResult(saveHandled2) + + // Reload for the last time. + val session3 = sessionRule.createOpenSession() + + session3.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onLoginSelect( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSelectOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + assertThat( + "There should be two options", + prompt.options.size, + equalTo(2), + ) + + var usernames = arrayOf(user1, user2) + var passwords = arrayOf(pass1, pass2) + + for (i in 0..1) { + val login = prompt.options[i].value + + assertThat("Login should not be null", login, notNullValue()) + assertThat( + "Username should match", + login.username, + equalTo(usernames[i]), + ) + assertThat( + "Password should match", + login.password, + equalTo(passwords[i]), + ) + } + + Handler(Looper.getMainLooper()).postDelayed({ + selectHandled.complete(null) + }, acceptDelay) + + return GeckoResult.fromValue(prompt.confirm(prompt.options[0])) + } + }) + + session3.loadTestPath(FORMS3_HTML_PATH) + session3.waitForPageStop() + + // Focus on the username input field. + session3.evaluateJS("document.querySelector('#user1').focus()") + sessionRule.waitForResult(selectHandled) + + assertThat( + "Filled username should match", + session3.evaluateJS("document.querySelector('#user1').value") as String, + equalTo(user1), + ) + + assertThat( + "Filled password should match", + session3.evaluateJS("document.querySelector('#pass1').value") as String, + equalTo(pass1), + ) + + // Submit the selection. + session3.evaluateJS("document.querySelector('#form1').submit()") + sessionRule.waitForResult(usedHandled) + } + + @Test + fun loginSelectModifyAccept() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "dom.disable_open_during_load" to false, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + // Test: + // 1. Load a login form page. + // 2. Input un/pw and submit. + // a. Ensure onLoginSave is called accordingly. + // b. Save the submitted login entry. + // 3. Reload the login form page. + // a. Ensure onLoginFetch is called. + // b. Return empty login entry list to avoid autofilling. + // 4. Input a new set of un/pw and submit. + // a. Ensure onLoginSave is called again. + // b. Save the submitted login entry. + // 5. Reload the login form page. + // 6. Focus on the username input field. + // a. Ensure onLoginFetch is called. + // b. Return the saved login entries. + // c. Ensure onLoginSelect is called. + // d. Select and return a new login entry. + // e. Submit the form. + // f. Ensure that onLoginUsed is not called. + + val user1 = "user1x" + val user2 = "user2x" + val pass1 = "pass1x" + val pass2 = "pass2x" + val userMod = "user1xmod" + val passMod = "pass1xmod" + val savedLogins = mutableListOf<LoginEntry>() + + val saveHandled1 = GeckoResult<Void>() + val saveHandled2 = GeckoResult<Void>() + val selectHandled = GeckoResult<Void>() + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + assertThat("Domain should match", domain, equalTo("localhost")) + + var logins = mutableListOf<LoginEntry>() + + if (savedLogins.size == 2) { + logins = savedLogins + } + + return GeckoResult.fromValue(logins.toTypedArray()) + } + + @AssertCalled(count = 2) + override fun onLoginSave(login: LoginEntry) { + var username = "" + var password = "" + var handle = GeckoResult<Void>() + + if (sessionRule.currentCall.counter == 1) { + username = user1 + password = pass1 + handle = saveHandled1 + } else if (sessionRule.currentCall.counter == 2) { + username = user2 + password = pass2 + handle = saveHandled2 + } + + val savedLogin = LoginEntry.Builder() + .guid(login.username) + .origin(login.origin) + .formActionOrigin(login.formActionOrigin) + .username(login.username) + .password(login.password) + .build() + + savedLogins.add(savedLogin) + + assertThat( + "Username should match", + login.username, + equalTo(username), + ) + + assertThat( + "Password should match", + login.password, + equalTo(password), + ) + + handle.complete(null) + } + + @AssertCalled(false) + override fun onLoginUsed(login: LoginEntry, usedFields: Int) {} + }) + + mainSession.loadTestPath(FORMS3_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.delegateDuringNextWait(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + val option = prompt.options[0] + val login = option.value + + assertThat("Login should not be null", login, notNullValue()) + + assertThat( + "Username should match", + login.username, + equalTo(user1), + ) + + assertThat( + "Password should match", + login.password, + equalTo(pass1), + ) + + return GeckoResult.fromValue(prompt.confirm(option)) + } + }) + + // Assign login credentials. + mainSession.evaluateJS("document.querySelector('#user1').value = '$user1'") + mainSession.evaluateJS("document.querySelector('#pass1').value = '$pass1'") + + // Submit the form. + mainSession.evaluateJS("document.querySelector('#form1').submit()") + sessionRule.waitForResult(saveHandled1) + + // Reload. + val session2 = sessionRule.createOpenSession() + session2.loadTestPath(FORMS3_HTML_PATH) + session2.waitForPageStop() + + session2.delegateDuringNextWait(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + val option = prompt.options[0] + val login = option.value + + assertThat("Login should not be null", login, notNullValue()) + + assertThat( + "Username should match", + login.username, + equalTo(user2), + ) + + assertThat( + "Password should match", + login.password, + equalTo(pass2), + ) + + return GeckoResult.fromValue(prompt.confirm(option)) + } + }) + + // Assign alternative login credentials. + session2.evaluateJS("document.querySelector('#user1').value = '$user2'") + session2.evaluateJS("document.querySelector('#pass1').value = '$pass2'") + + // Submit the form. + session2.evaluateJS("document.querySelector('#form1').submit()") + sessionRule.waitForResult(saveHandled2) + + // Reload for the last time. + val session3 = sessionRule.createOpenSession() + + session3.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled(count = 1) + override fun onLoginSelect( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSelectOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + assertThat( + "There should be two options", + prompt.options.size, + equalTo(2), + ) + + var usernames = arrayOf(user1, user2) + var passwords = arrayOf(pass1, pass2) + + for (i in 0..1) { + val login = prompt.options[i].value + + assertThat("Login should not be null", login, notNullValue()) + assertThat( + "Username should match", + login.username, + equalTo(usernames[i]), + ) + assertThat( + "Password should match", + login.password, + equalTo(passwords[i]), + ) + } + + val login = prompt.options[0].value + val modOption = LoginSelectOption( + LoginEntry.Builder() + .origin(login.origin) + .formActionOrigin(login.formActionOrigin) + .username(userMod) + .password(passMod) + .build(), + ) + + Handler(Looper.getMainLooper()).postDelayed({ + selectHandled.complete(null) + }, acceptDelay) + + return GeckoResult.fromValue(prompt.confirm(modOption)) + } + }) + + session3.loadTestPath(FORMS3_HTML_PATH) + session3.waitForPageStop() + + // Focus on the username input field. + session3.evaluateJS("document.querySelector('#user1').focus()") + sessionRule.waitForResult(selectHandled) + + assertThat( + "Filled username should match", + session3.evaluateJS("document.querySelector('#user1').value") as String, + equalTo(userMod), + ) + + assertThat( + "Filled password should match", + session3.evaluateJS("document.querySelector('#pass1').value") as String, + equalTo(passMod), + ) + + // Submit the selection. + session3.evaluateJS("document.querySelector('#form1').submit()") + session3.waitForPageStop() + } + + @Test + fun loginSelectGeneratedPassword() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "signon.generation.enabled" to true, + "signon.generation.available" to true, + "dom.disable_open_during_load" to false, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + // Test: + // 1. Load a login form page. + // 2. Input username. + // 3. Focus on the password input field. + // a. Ensure onLoginSelect is called with a generated password. + // b. Return the login entry with the generated password. + // 4. Submit the login form. + // a. Ensure onLoginSave is called with accordingly. + + val user1 = "user1x" + var genPass = "" + + val saveHandled1 = GeckoResult<Void>() + val selectHandled = GeckoResult<Void>() + var numSelects = 0 + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + assertThat("Domain should match", domain, equalTo("localhost")) + + return GeckoResult.fromValue(null) + } + + @AssertCalled(count = 1) + override fun onLoginSave(login: LoginEntry) { + assertThat( + "Username should match", + login.username, + equalTo(user1), + ) + + assertThat( + "Password should match", + login.password, + equalTo(genPass), + ) + + saveHandled1.complete(null) + } + + @AssertCalled(false) + override fun onLoginUsed(login: LoginEntry, usedFields: Int) {} + }) + + mainSession.loadTestPath(FORMS4_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled + override fun onLoginSelect( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSelectOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + assertThat( + "There should be one option", + prompt.options.size, + equalTo(1), + ) + + val option = prompt.options[0] + val login = option.value + + assertThat( + "Hint should match", + option.hint, + equalTo(SelectOption.Hint.GENERATED), + ) + + assertThat("Login should not be null", login, notNullValue()) + assertThat( + "Password should not be empty", + login.password, + not(isEmptyOrNullString()), + ) + + genPass = login.password + + if (numSelects == 0) { + Handler(Looper.getMainLooper()).postDelayed({ + selectHandled.complete(null) + }, acceptDelay) + } + ++numSelects + + return GeckoResult.fromValue(prompt.confirm(option)) + } + + @AssertCalled(count = 1) + override fun onLoginSave( + session: GeckoSession, + prompt: AutocompleteRequest<LoginSaveOption>, + ): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat("Session should not be null", session, notNullValue()) + + val option = prompt.options[0] + val login = option.value + + assertThat("Login should not be null", login, notNullValue()) + + assertThat( + "Username should match", + login.username, + equalTo(user1), + ) + + // TODO: The flag is only set for login entry updates yet. + /* + assertThat( + "Hint should match", + option.hint, + equalTo(LoginSaveOption.Hint.GENERATED)) + */ + + assertThat( + "Password should not be empty", + login.password, + not(isEmptyOrNullString()), + ) + + assertThat( + "Password should match", + login.password, + equalTo(genPass), + ) + + return GeckoResult.fromValue(prompt.confirm(option)) + } + }) + + // Assign username and focus on password. + mainSession.evaluateJS("document.querySelector('#user1').value = '$user1'") + mainSession.evaluateJS("document.querySelector('#pass1').focus()") + sessionRule.waitForResult(selectHandled) + + assertThat( + "Filled username should match", + mainSession.evaluateJS("document.querySelector('#user1').value") as String, + equalTo(user1), + ) + + val filledPass = mainSession.evaluateJS( + "document.querySelector('#pass1').value", + ) as String + + assertThat( + "Password should not be empty", + filledPass, + not(isEmptyOrNullString()), + ) + + assertThat( + "Filled password should match", + filledPass, + equalTo(genPass), + ) + + // Submit the selection. + mainSession.evaluateJS("document.querySelector('#form1').submit()") + mainSession.waitForPageStop() + } + + @Test + fun loginSelectDismiss() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + // Enable login management since it's disabled in automation. + "signon.rememberSignons" to true, + "signon.autofillForms.http" to true, + "signon.userInputRequiredToCapture.enabled" to false, + ), + ) + + val user = arrayOf("user1x", "user2x") + val pass = arrayOf("pass1x", "pass2x") + val guid = arrayOf("test-guid1", "test-guid2") + val origin = GeckoSessionTestRule.TEST_ENDPOINT + val savedLogins = arrayOf( + LoginEntry.Builder() + .guid(guid[0]) + .origin(origin) + .formActionOrigin(origin) + .username(user[0]) + .password(pass[0]) + .build(), + LoginEntry.Builder() + .guid(guid[1]) + .origin(origin) + .formActionOrigin(origin) + .username(user[1]) + .password(pass[1]) + .build(), + ) + + sessionRule.delegateUntilTestEnd(object : StorageDelegate { + @AssertCalled + override fun onLoginFetch(domain: String): GeckoResult<Array<LoginEntry>>? { + return GeckoResult.fromValue(savedLogins) + } + }) + + val result = GeckoResult<PromptDelegate.PromptResponse>() + val promptInstanceDelegate = object : PromptDelegate.PromptInstanceDelegate { + override fun onPromptDismiss(prompt: PromptDelegate.BasePrompt) { + result.complete(prompt.dismiss()) + } + } + + val promptHandled = GeckoResult<Void>() + mainSession.delegateUntilTestEnd(object : PromptDelegate { + @AssertCalled + override fun onLoginSelect(session: GeckoSession, prompt: AutocompleteRequest<LoginSelectOption>): GeckoResult<PromptDelegate.PromptResponse>? { + assertThat( + "There should be two options", + prompt.options.size, + equalTo(2), + ) + prompt.setDelegate(promptInstanceDelegate) + Handler(Looper.getMainLooper()).postDelayed({ + promptHandled.complete(null) + }, acceptDelay) + + return GeckoResult() + } + }) + + mainSession.loadTestPath(FORMS3_HTML_PATH) + mainSession.waitForPageStop() + + mainSession.evaluateJS("document.querySelector('#user1').focus()") + sessionRule.waitForResult(promptHandled) + mainSession.evaluateJS("document.querySelector('#user1').blur()") + sessionRule.waitForResult(result) + } +} |