From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../browser/address/browser_address_telemetry.js | 691 +++++++++++++++++++++ 1 file changed, 691 insertions(+) create mode 100644 browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js (limited to 'browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js') diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js b/browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js new file mode 100644 index 0000000000..66dbc24951 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/address/browser_address_telemetry.js @@ -0,0 +1,691 @@ +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const { AddressTelemetry } = ChromeUtils.import( + "resource://autofill/AutofillTelemetry.jsm" +); + +// Preference definitions +const ENABLED_PREF = ENABLED_AUTOFILL_ADDRESSES_PREF; +const AVAILABLE_PREF = AUTOFILL_ADDRESSES_AVAILABLE_PREF; +const CAPTURE_ENABLE_PREF = ENABLED_AUTOFILL_ADDRESSES_CAPTURE_PREF; + +// Telemetry definitions +const EVENT_CATEGORY = "address"; + +const SCALAR_DETECTED_SECTION_COUNT = + "formautofill.addresses.detected_sections_count"; +const SCALAR_SUBMITTED_SECTION_COUNT = + "formautofill.addresses.submitted_sections_count"; +const SCALAR_AUTOFILL_PROFILE_COUNT = + "formautofill.addresses.autofill_profiles_count"; + +const HISTOGRAM_PROFILE_NUM_USES = "AUTOFILL_PROFILE_NUM_USES"; +const HISTOGRAM_PROFILE_NUM_USES_KEY = "address"; + +// Autofill UI +const MANAGE_DIALOG_URL = MANAGE_ADDRESSES_DIALOG_URL; +const EDIT_DIALOG_URL = EDIT_ADDRESS_DIALOG_URL; +const DIALOG_SIZE = "width=600,height=400"; +const MANAGE_RECORD_SELECTOR = "#addresses"; + +// Test specific definitions +const TEST_PROFILE = TEST_ADDRESS_1; +const TEST_PROFILE_1 = TEST_ADDRESS_1; +const TEST_PROFILE_2 = TEST_ADDRESS_2; +const TEST_PROFILE_3 = TEST_ADDRESS_3; + +const TEST_FOCUS_NAME_FIELD = "given-name"; +const TEST_FOCUS_NAME_FIELD_SELECTOR = "#" + TEST_FOCUS_NAME_FIELD; + +// Used for tests that update address fields after filling +const TEST_NEW_VALUES = { + "#given-name": "Test User", + "#organization": "Sesame Street", + "#street-address": "123 Sesame Street", + "#tel": "1-345-345-3456", +}; + +const TEST_UPDATE_PROFILE_2_VALUES = { + "#email": "profile2@mozilla.org", +}; +const TEST_BASIC_ADDRESS_FORM_URL = ADDRESS_FORM_URL; +const TEST_BASIC_ADDRESS_FORM_WITHOUT_AC_URL = + ADDRESS_FORM_WITHOUT_AUTOCOMPLETE_URL; +// This should be sync with the address fields that appear in TEST_BASIC_ADDRESS_FORM +const TEST_BASIC_ADDRESS_FORM_FIELDS = [ + "street_address", + "address_level1", + "address_level2", + "postal_code", + "country", + "given_name", + "family_name", + "organization", + "email", + "tel", +]; + +function buildFormExtra(list, fields, fieldValue, defaultValue, aExtra = {}) { + let extra = {}; + for (const field of list) { + if (aExtra[field]) { + extra[field] = aExtra[field]; + } else { + extra[field] = fields.includes(field) ? fieldValue : defaultValue; + } + } + return extra; +} + +/** + * Utility function to generate expected value for `address_form` and `address_form_ext` + * telemetry event. + * + * @param {string} method see `methods` in `address_form` event telemetry + * @param {object} defaultExtra default extra object, this will not be overwritten + * @param {object} fields address fields that will be set to `value` param + * @param {string} value value to set for fields list in `fields` argument + * @param {string} defaultValue value to set for address fields that are not listed in `fields` argument` + */ +function formArgs( + method, + defaultExtra, + fields = [], + value = undefined, + defaultValue = null +) { + if (["popup_shown", "filled_modified"].includes(method)) { + return [["address", method, "address_form", undefined, defaultExtra]]; + } + let extra = buildFormExtra( + AddressTelemetry.SUPPORTED_FIELDS_IN_FORM, + fields, + value, + defaultValue, + defaultExtra + ); + + let extraExt = buildFormExtra( + AddressTelemetry.SUPPORTED_FIELDS_IN_FORM_EXT, + fields, + value, + defaultValue, + defaultExtra + ); + + // The order here should sync with AutofillTelemetry. + return [ + ["address", method, "address_form", undefined, extra], + ["address", method, "address_form_ext", undefined, extraExt], + ]; +} + +function getProfiles() { + return getAddresses(); +} + +async function assertTelemetry(expected_content, expected_parent) { + let snapshots; + + info( + `Waiting for ${expected_content?.length ?? 0} content events and ` + + `${expected_parent?.length ?? 0} parent events` + ); + + await TestUtils.waitForCondition( + () => { + snapshots = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + + return ( + (snapshots.parent?.length ?? 0) >= (expected_parent?.length ?? 0) && + (snapshots.content?.length ?? 0) >= (expected_content?.length ?? 0) + ); + }, + "Wait for telemetry to be collected", + 100, + 100 + ); + + info(JSON.stringify(snapshots, null, 2)); + + if (expected_content !== undefined) { + expected_content = expected_content.map( + ([category, method, object, value, extra]) => { + return { category, method, object, value, extra }; + } + ); + + let clear = expected_parent === undefined; + + TelemetryTestUtils.assertEvents( + expected_content, + { + category: EVENT_CATEGORY, + }, + { clear, process: "content" } + ); + } + + if (expected_parent !== undefined) { + expected_parent = expected_parent.map( + ([category, method, object, value, extra]) => { + return { category, method, object, value, extra }; + } + ); + TelemetryTestUtils.assertEvents( + expected_parent, + { + category: EVENT_CATEGORY, + }, + { process: "parent" } + ); + } +} + +function _assertHistogram(snapshot, expectedNonZeroRanges) { + // Compute the actual ranges in the format { range1: value1, range2: value2 }. + let actualNonZeroRanges = {}; + for (let [range, value] of Object.entries(snapshot.values)) { + if (value > 0) { + actualNonZeroRanges[range] = value; + } + } + + // These are stringified to visualize the differences between the values. + Assert.equal( + JSON.stringify(actualNonZeroRanges), + JSON.stringify(expectedNonZeroRanges) + ); +} + +function assertKeyedHistogram(histogramId, key, expected) { + let snapshot = Services.telemetry + .getKeyedHistogramById(histogramId) + .snapshot(); + + if (expected == undefined) { + Assert.deepEqual(snapshot, {}); + } else { + _assertHistogram(snapshot[key], expected); + } +} + +async function openTabAndUseAutofillProfile( + idx, + profile, + { closeTab = true, submitForm = true } = {} +) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_BASIC_ADDRESS_FORM_URL + ); + let browser = tab.linkedBrowser; + + await openPopupOn(browser, "form " + TEST_FOCUS_NAME_FIELD_SELECTOR); + + for (let i = 0; i <= idx; i++) { + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + } + + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + TEST_FOCUS_NAME_FIELD_SELECTOR, + profile[TEST_FOCUS_NAME_FIELD] + ); + await focusUpdateSubmitForm( + browser, + { + focusSelector: TEST_FOCUS_NAME_FIELD_SELECTOR, + newValues: {}, + }, + submitForm + ); + + if (!closeTab) { + return tab; + } + + await BrowserTestUtils.removeTab(tab); + return null; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [ENABLED_PREF, true], + [AVAILABLE_PREF, "on"], + [CAPTURE_ENABLE_PREF, true], + ], + }); + + Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, true); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + registerCleanupFunction(() => { + Services.telemetry.setEventRecordingEnabled(EVENT_CATEGORY, false); + }); +}); + +add_task(async function test_popup_opened() { + await setStorage(TEST_PROFILE); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + const focusInput = TEST_FOCUS_NAME_FIELD_SELECTOR; + + await openPopupOn(browser, focusInput); + + // Clean up + await closePopup(browser); + } + ); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + await assertTelemetry([ + ...formArgs("detected", {}, fields, "true", "false"), + ...formArgs("popup_shown", { field_name: TEST_FOCUS_NAME_FIELD }), + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_DETECTED_SECTION_COUNT, + 1, + "There should be 1 section detected." + ); + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_SUBMITTED_SECTION_COUNT, + 1 + ); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_popup_opened_form_without_autocomplete() { + await setStorage(TEST_PROFILE); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_WITHOUT_AC_URL }, + async function (browser) { + const focusInput = TEST_FOCUS_NAME_FIELD_SELECTOR; + await openPopupOn(browser, focusInput); + await closePopup(browser); + } + ); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + await assertTelemetry([ + ...formArgs("detected", {}, fields, "0", "false"), + ...formArgs("popup_shown", { field_name: TEST_FOCUS_NAME_FIELD }), + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_DETECTED_SECTION_COUNT, + 1, + "There should be 1 section detected." + ); + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_SUBMITTED_SECTION_COUNT + ); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_submit_autofill_profile_new() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.formautofill.firstTimeUse", true]], + }); + async function test_per_command( + command, + idx, + useCount = {}, + expectChanged = undefined + ) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let onChanged; + if (expectChanged !== undefined) { + onChanged = TestUtils.topicObserved("formautofill-storage-changed"); + } + + await focusUpdateSubmitForm(browser, { + focusSelector: TEST_FOCUS_NAME_FIELD_SELECTOR, + newValues: TEST_NEW_VALUES, + }); + + await onPopupShown; + await clickDoorhangerButton(command, idx); + if (expectChanged !== undefined) { + await onChanged; + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + SCALAR_AUTOFILL_PROFILE_COUNT, + expectChanged, + "There should be ${expectChanged} profile(s) stored." + ); + } + } + ); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + useCount + ); + + await removeAllRecords(); + } + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + Services.telemetry.getKeyedHistogramById(HISTOGRAM_PROFILE_NUM_USES).clear(); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + let expected_content = [ + ...formArgs("detected", {}, fields, "true", "false"), + ...formArgs("submitted", {}, fields, "user_filled", "unavailable"), + ]; + + // FTU + await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1); + await assertTelemetry(expected_content, [ + [EVENT_CATEGORY, "show", "capture_doorhanger"], + [EVENT_CATEGORY, "pref", "capture_doorhanger"], + ]); + + // Need to close preference tab + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_DETECTED_SECTION_COUNT, + 1, + "There should be 1 sections detected." + ); + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + SCALAR_SUBMITTED_SECTION_COUNT, + 1, + "There should be 1 section submitted." + ); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_submit_autofill_profile_update() { + async function test_per_command( + command, + idx, + useCount = {}, + expectChanged = undefined + ) { + await setStorage(TEST_PROFILE_2); + let profiles = await getProfiles(); + Assert.equal(profiles.length, 1, "1 entry in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_BASIC_ADDRESS_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let onChanged; + if (expectChanged !== undefined) { + onChanged = TestUtils.topicObserved("formautofill-storage-changed"); + } + + await openPopupOn(browser, TEST_FOCUS_NAME_FIELD_SELECTOR); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + + await waitForAutofill( + browser, + TEST_FOCUS_NAME_FIELD_SELECTOR, + TEST_PROFILE_2[TEST_FOCUS_NAME_FIELD] + ); + await focusUpdateSubmitForm(browser, { + focusSelector: TEST_FOCUS_NAME_FIELD_SELECTOR, + newValues: TEST_UPDATE_PROFILE_2_VALUES, + }); + await onPopupShown; + await clickDoorhangerButton(command, idx); + if (expectChanged !== undefined) { + await onChanged; + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + SCALAR_AUTOFILL_PROFILE_COUNT, + expectChanged, + "There should be ${expectChanged} profile(s) stored." + ); + } + } + ); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + useCount + ); + + SpecialPowers.clearUserPref(ENABLED_PREF); + + await removeAllRecords(); + } + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + Services.telemetry.getKeyedHistogramById(HISTOGRAM_PROFILE_NUM_USES).clear(); + + const fields = TEST_BASIC_ADDRESS_FORM_FIELDS; + let expected_content = [ + ...formArgs("detected", {}, fields, "true", "false"), + ...formArgs("popup_shown", { field_name: TEST_FOCUS_NAME_FIELD }), + ...formArgs( + "filled", + { + given_name: "filled", + street_address: "filled", + country: "filled", + }, + fields, + "not_filled", + "unavailable" + ), + ...formArgs( + "submitted", + { + given_name: "autofilled", + street_address: "autofilled", + country: "autofilled", + email: "user_filled", + }, + fields, + "not_filled", + "unavailable" + ), + ]; + + await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1); + await assertTelemetry(expected_content, [ + [EVENT_CATEGORY, "show", "update_doorhanger"], + [EVENT_CATEGORY, "update", "update_doorhanger"], + ]); + + await test_per_command(SECONDARY_BUTTON, undefined, { 0: 1, 1: 1 }, 2); + await assertTelemetry(expected_content, [ + [EVENT_CATEGORY, "show", "update_doorhanger"], + [EVENT_CATEGORY, "save", "update_doorhanger"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_removingAutofillProfilesViaKeyboardDelete() { + await setStorage(TEST_PROFILE); + + let win = window.openDialog(MANAGE_DIALOG_URL, null, DIALOG_SIZE); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(MANAGE_RECORD_SELECTOR); + Assert.equal(selRecords.length, 1, "One entry"); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + EventUtils.synthesizeKey("VK_DELETE", {}, win); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved"); + Assert.equal(selRecords.length, 0, "No entry left"); + + win.close(); + + await assertTelemetry(undefined, [ + [EVENT_CATEGORY, "show", "manage"], + [EVENT_CATEGORY, "delete", "manage"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_saveAutofillProfile() { + Services.telemetry.clearEvents(); + + await testDialog(EDIT_DIALOG_URL, win => { + // TODP: Default to US because the layout will be different + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["given-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["family-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["street-address"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["address-level2"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_PROFILE["postal-code"], {}, win); + info("saving one entry"); + win.document.querySelector("#save").click(); + }); + + await assertTelemetry(undefined, [[EVENT_CATEGORY, "add", "manage"]]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_editAutofillProfile() { + Services.telemetry.clearEvents(); + + await setStorage(TEST_PROFILE); + + let profiles = await getProfiles(); + Assert.equal(profiles.length, 1, "1 entry in storage"); + await testDialog( + EDIT_DIALOG_URL, + win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("test", {}, win); + win.document.querySelector("#save").click(); + }, + { + record: profiles[0], + } + ); + + await assertTelemetry(undefined, [ + [EVENT_CATEGORY, "show_entry", "manage"], + [EVENT_CATEGORY, "edit", "manage"], + ]); + + await removeAllRecords(); + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_histogram() { + Services.telemetry.getKeyedHistogramById(HISTOGRAM_PROFILE_NUM_USES).clear(); + + await setStorage(TEST_PROFILE_1, TEST_PROFILE_2, TEST_PROFILE_3); + let profiles = await getProfiles(); + Assert.equal(profiles.length, 3, "3 entry in storage"); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 3 } + ); + + await openTabAndUseAutofillProfile(0, TEST_PROFILE_1); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 2, 1: 1 } + ); + + await openTabAndUseAutofillProfile(1, TEST_PROFILE_2); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 1, 1: 2 } + ); + + await openTabAndUseAutofillProfile(0, TEST_PROFILE_2); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 1, 1: 1, 2: 1 } + ); + + await openTabAndUseAutofillProfile(1, TEST_PROFILE_1); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 0: 1, 2: 2 } + ); + + await openTabAndUseAutofillProfile(2, TEST_PROFILE_3); + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + { 1: 1, 2: 2 } + ); + + await removeAllRecords(); + + assertKeyedHistogram( + HISTOGRAM_PROFILE_NUM_USES, + HISTOGRAM_PROFILE_NUM_USES_KEY, + undefined + ); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); +}); + +add_task(async function test_clear_autofill_profile_autofill() { + // Address does not have clear pref. Keep the test so we know we should implement + // the test if we support clearing address via autocomplete. + Assert.ok(true); +}); -- cgit v1.2.3