<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Test autofill and autocomplete on autocomplete=new-password fields</title>
  <!-- This test assumes that telemetry events are not cleared after the `setup` task. -->
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <script src="/tests/SimpleTest/EventUtils.js"></script>
  <script src="pwmgr_common.js"></script>
  <script src="../../../satchel/test/satchel_common.js"></script>
  <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
</head>
<body>
Login Manager test: autofill with autocomplete=new-password fields

<p id="display"></p>

<!-- we presumably can't hide the content for this test. -->
<div id="content"></div>

<pre id="test">
<script class="testbody" type="text/javascript">
const { ContentTaskUtils } = SpecialPowers.ChromeUtils.import(
  "resource://testing-common/ContentTaskUtils.jsm"
);
const { TestUtils } = SpecialPowers.ChromeUtils.import(
  "resource://testing-common/TestUtils.jsm"
);

let dateAndTimeFormatter = new SpecialPowers.Services.intl.DateTimeFormat(undefined, {
  dateStyle: "medium",
});

const TelemetryFilterPropsUsed = Object.freeze({
  category: "pwmgr",
  method: "autocomplete_field",
  object: "generatedpassword",
});

const TelemetryFilterPropsShown = Object.freeze({
  category: "pwmgr",
  method: "autocomplete_shown",
  object: "generatedpassword",
});

async function waitForTelemetryEventsCondition(cond, options = {},
                                               errorMsg = "waitForTelemetryEventsCondition timed out", maxTries = 200) {
  return TestUtils.waitForCondition(async () => {
    let events = await getTelemetryEvents(options);
    let result;
    try {
      result = cond(events);
      info("waitForTelemetryEventsCondition, result: " + result);
    } catch (e) {
      info("waitForTelemetryEventsCondition caught exception, got events: " + JSON.stringify(events));
      ok(false, `${e}\n${e.stack}`);
    }
    return result ? events : null;
  }, errorMsg, maxTries);
}

async function showACPopup(formNumber, expectedACLabels) {
  const autocompleteItems = await popupByArrowDown();
  checkAutoCompleteResults(autocompleteItems, expectedACLabels,
                           window.location.host, "Check all rows are correct");
}

async function checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents) {
  info(`showed generated password option, check there are now ${expectedPWGenTelemetryEvents} generatedpassword telemetry events`);
  await waitForTelemetryEventsCondition(events => {
    return events.length == expectedPWGenTelemetryEvents;
  }, { process: "parent", filterProps: TelemetryFilterPropsShown }, `Wait for there to be ${expectedPWGenTelemetryEvents} shown telemetry events`);
}

async function checkTelemetryEventsPWGenUsed(expectedPWGenTelemetryEvents) {
  info("filled generated password again, ensure we don't record another generatedpassword autocomplete telemetry event");
  let telemetryEvents;
  try {
    telemetryEvents = await waitForTelemetryEventsCondition(events => events.length == expectedPWGenTelemetryEvents + 1,
                                                            { process: "parent", filterProps: TelemetryFilterPropsUsed },
                                                            `Wait for there to be ${expectedPWGenTelemetryEvents + 1} used events`, 50);
  } catch (ex) {}
  ok(!telemetryEvents, `Expected to timeout waiting for there to be ${expectedPWGenTelemetryEvents + 1} events`);
}

function clearGeneratedPasswords() {
  const { LoginManagerParent } = ChromeUtils.import("resource://gre/modules/LoginManagerParent.jsm");
  if (LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin()) {
    LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear();
  }
}

add_setup(async () => {
  let useEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsUsed, clear: true });
  is(useEvents.length, 0, "Expect 0 use events");
  let showEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsShown, clear: true });
  is(showEvents.length, 0, "Expect 0 show events");
  let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
  is(acEvents.length, 0, "Expect 0 autocomplete events");

  await SpecialPowers.pushPrefEnv({"set": [
    ["signon.generation.available", true],
    ["signon.generation.enabled", true],
  ]});
});

add_task(async function test_autofillAutocompleteUsername_noGeneration() {
  await SpecialPowers.pushPrefEnv({"set": [
    ["signon.generation.available", false],
    ["signon.generation.enabled", false],
  ]});
  await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);

  createLoginForm({
    num: 1,
    action: "https://autofill",
    password: {
      name: "p"
    }
  });
  const form2 = createLoginForm({
    num: 2,
    action: "https://autofill",
    password: {
      name: "password",
      autocomplete: "new-password"
    }
  });
  await promiseFormsProcessedInSameProcess(2);

  // reference form was filled as expected?
  checkForm(1, "user1", "pass1");

  // 2nd form should not be filled
  checkForm(2, "", "");

  form2.uname.focus();
  await showACPopup(2, ["user1"]);

  let acEvents = await waitForTelemetryEventsCondition(events => {
    return events.length == 1;
  }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`);
  checkACTelemetryEvent(acEvents[0], form2.uname, {
    "hadPrevious": "0",
    "login": "1",
    "loginsFooter": "1"
  });

  synthesizeKey("KEY_ArrowDown");
  synthesizeKey("KEY_Enter");

  await promiseFormsProcessedInSameProcess();
  checkForm(2, "user1", "pass1");

  await SpecialPowers.popPrefEnv();
});

add_task(async function test_autofillAutocompletePassword_noGeneration() {
  await SpecialPowers.pushPrefEnv({"set": [
    ["signon.generation.available", false],
    ["signon.generation.enabled", false],
  ]});
  await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);

  const form = createLoginForm({
    num: 2,
    action: "https://autofill",
    password: {
      name: "password",
      autocomplete: "new-password"
    }
  });
  await promiseFormsProcessedInSameProcess();

  // 2nd form should not be filled
  checkForm(2, "", "");

  form.password.focus();
  await showACPopup(2, ["user1"]);
  let acEvents = await waitForTelemetryEventsCondition(events => {
    return events.length == 1;
  }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`);
  checkACTelemetryEvent(acEvents[0], form.password, {
    "hadPrevious": "0",
    "login": "1",
    "loginsFooter": "1"
  });

  synthesizeKey("KEY_ArrowDown");
  synthesizeKey("KEY_Enter");
  // Can't use promiseFormsProcessedInSameProcess() when autocomplete fills the field directly.
  await SimpleTest.promiseWaitForCondition(() => form.password.value == "pass1", "Check pw filled");
  checkForm(2, "", "pass1");

  // No autocomplete results should appear for non-empty pw fields.
  await noPopupByArrowDown();

  await SpecialPowers.popPrefEnv();
});

add_task(async function test_autofillAutocompleteUsername_noGeneration2() {
  await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);

  const form = createLoginForm({
    num: 2,
    action: "https://autofill",
    password: {
      name: "password",
      autocomplete: "new-password"
    }
  });
  await promiseFormsProcessedInSameProcess();

  // 2nd form should not be filled
  checkForm(2, "", "");

  form.uname.focus();
  // No generation option on username fields.
  await showACPopup(2, ["user1"]);
  let acEvents = await waitForTelemetryEventsCondition(events => {
    return events.length == 1;
  }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`);
  checkACTelemetryEvent(acEvents[0], form.uname, {
    "hadPrevious": "0",
    "login": "1",
    "loginsFooter": "1"
  });

  synthesizeKey("KEY_ArrowDown");
  synthesizeKey("KEY_Enter");
  await promiseFormsProcessedInSameProcess();
  checkForm(2, "user1", "pass1");
});

add_task(async function test_autofillAutocompletePassword_withGeneration() {
  const formAttributesToTest = [
    {
      num: 2,
      action: "https://autofill",
      password: {
        name: "password",
        autocomplete: "new-password"
      }
    },
    {
      num: 3,
      action: "https://autofill",
      username: {
        name: "username"
      },
      password: {
        name: "password",
        label: "New password"
      }
    }
  ];

  // Bug 1616356 and Bug 1548878: Recorded once per origin
  let expectedPWGenTelemetryEvents = 0;
   // Bug 1619498: Recorded once every time the autocomplete popup is shown
  let expectedACShownTelemetryEvents = 0;

  for (const formAttributes of formAttributesToTest) {
    runInParent(clearGeneratedPasswords);
    await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);

    const formNumber = formAttributes.num;

    const form = createLoginForm(formAttributes);
    await promiseFormsProcessedInSameProcess();
    form.reset();

    // This form should be filled
    checkForm(formNumber, "", "");

    form.password.focus();

    await showACPopup(formNumber, [
      "user1",
      "Use a Securely Generated Password",
    ]);
    expectedPWGenTelemetryEvents++;
    expectedACShownTelemetryEvents++;

    await checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents);
    let acEvents = await waitForTelemetryEventsCondition(events => {
      return events.length == expectedACShownTelemetryEvents;
    }, { process: "parent", filterProps: TelemetryFilterPropsAC }, `Wait for there to be ${expectedACShownTelemetryEvents} autocomplete telemetry event(s)`);
    checkACTelemetryEvent(acEvents[expectedACShownTelemetryEvents - 1], form.password, {
      "generatedPasswo": "1",
      "hadPrevious": "0",
      "login": "1",
      "loginsFooter": "1"
    });

    synthesizeKey("KEY_ArrowDown");
    synthesizeKey("KEY_Enter");
    // Can't use promiseFormsProcessedInSameProcess() when autocomplete fills the field directly.
    await SimpleTest.promiseWaitForCondition(() => form.password.value == "pass1", "Check pw filled");
    checkForm(formNumber, "", "pass1");

    // No autocomplete results should appear for non-empty pw fields.
    await noPopupByArrowDown();

    info("Removing all logins to test auto-saving of generated passwords");
    await LoginManager.removeAllUserFacingLogins();

    while (form.password.value) {
      synthesizeKey("KEY_Backspace");
    }
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field");

    info("This time select the generated password");
    await showACPopup(formNumber, [
      "Use a Securely Generated Password",
    ]);
    expectedACShownTelemetryEvents++;

    await checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents);
    acEvents = await waitForTelemetryEventsCondition(events => {
      return events.length == expectedACShownTelemetryEvents;
    }, { process: "parent", filterProps: TelemetryFilterPropsAC }, `Wait for there to be ${expectedACShownTelemetryEvents} autocomplete telemetry event(s)`);
    checkACTelemetryEvent(acEvents[expectedACShownTelemetryEvents - 1], form.password, {
      "generatedPasswo": "1",
      "hadPrevious": "0",
      "loginsFooter": "1"
    });

    synthesizeKey("KEY_ArrowDown");
    let storageAddPromise = promiseStorageChanged(["addLogin"]);
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Before first fill of generated pw");
    synthesizeKey("KEY_Enter");

    info("waiting for the password field to be filled with the generated password");
    await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled");
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After first fill of generated pw");
    info("Wait for generated password to be added to storage");
    await storageAddPromise;

    let logins = await LoginManager.getAllLogins();
    let timePasswordChanged = logins[logins.length - 1].timePasswordChanged;
    let time = dateAndTimeFormatter.format(new Date(timePasswordChanged));
    const LABEL_NO_USERNAME = "No username (" + time + ")";

    let generatedPW = form.password.value;
    is(generatedPW.length, GENERATED_PASSWORD_LENGTH, "Check generated password length");
    ok(generatedPW.match(GENERATED_PASSWORD_REGEX), "Check generated password format");
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After fill");

    info("Check field is masked upon blurring");
    synthesizeKey("KEY_Tab"); // blur
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "After blur");
    synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After shift-tab to focus again");
    // Remove selection for OS where the whole value is selected upon focus.
    synthesizeKey("KEY_ArrowRight");

    while (form.password.value) {
      LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, form.password.value);
      synthesizeKey("KEY_Backspace");
    }
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field");

    info("Blur the empty field to trigger a 'change' event");
    synthesizeKey("KEY_Tab"); // blur
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after blanking");
    synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after blanking");

    info("Type a single character after blanking");
    synthesizeKey("@");

    info("Blur the single-character field to trigger a 'change' event");
    synthesizeKey("KEY_Tab"); // blur
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after backspacing");
    synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after backspacing");
    synthesizeKey("KEY_Backspace"); // Blank the field again

    await showACPopup(formNumber, [
      LABEL_NO_USERNAME,
      "Use a Securely Generated Password",
    ]);
    expectedACShownTelemetryEvents++;

    synthesizeKey("KEY_ArrowDown");
    synthesizeKey("KEY_ArrowDown");
    synthesizeKey("KEY_Enter");
    await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled");
    // Same generated password should be used, even despite the 'change' to @ earlier.
    checkForm(formNumber, "", generatedPW);
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "Second fill of the generated pw");

    await checkTelemetryEventsPWGenUsed(expectedPWGenTelemetryEvents);

    logins = await LoginManager.getAllLogins();
    is(logins.length, 1, "Still 1 login after filling the generated password a 2nd time");
    is(logins[0].timePasswordChanged, timePasswordChanged, "Saved login wasn't changed");
    is(logins[0].password, generatedPW, "Password is the same");

    info("filling the saved login to ensure the field is masked again");

    while (form.password.value) {
      LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, form.password.value);
      synthesizeKey("KEY_Backspace");
    }
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field again");

    info("Blur the field to trigger a 'change' event again");
    synthesizeKey("KEY_Tab"); // blur
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after blanking again");
    synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after blanking again");
    // Remove selection for OS where the whole value is selected upon focus.
    synthesizeKey("KEY_ArrowRight");

    await showACPopup(formNumber, [
      LABEL_NO_USERNAME,
      "Use a Securely Generated Password",
    ]);
    expectedACShownTelemetryEvents++;

    synthesizeKey("KEY_ArrowDown");
    synthesizeKey("KEY_Enter");
    await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check saved generated pw filled");
    // Same generated password should be used but from storage
    checkForm(formNumber, "", generatedPW);
    // Passwords from storage should always be masked.
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after fill from storage");
    synthesizeKey("KEY_Tab"); // blur
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after blur");
    synthesizeKey("KEY_Tab", { shiftKey: true }); // focus
    LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after shift-tab to focus again");
  }
});

add_task(async function test_autofillAutocompletePassword_saveLoginDisabled() {
  await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);

  const form = createLoginForm({
    num: 2,
    action: "https://autofill",
    password: {
      name: "password",
      autocomplete: "new-password"
    }
  });
  await promiseFormsProcessedInSameProcess();

  // form should not be filled
  checkForm(2, "", "");

  let formOrigin = new URL(document.documentURI).origin;
  is(formOrigin, location.origin, "Expected form origin");

  await LoginManager.setLoginSavingEnabled(location.origin, false);

  form.password.focus();
  // when login-saving is disabled for an origin, we expect no generated password row here
  await showACPopup(2, ["user1"]);

  // close any open menu
  synthesizeKey("KEY_Escape");
  await untilAutocompletePopupClosed();

  await LoginManager.setLoginSavingEnabled(location.origin, true);
});

add_task(async function test_deleteAndReselectGeneratedPassword() {
  await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]);

  const form = createLoginForm({
    num: 2,
    action: "https://autofill",
    password: {
      name: "password",
      autocomplete: "new-password"
    }
  });
  await promiseFormsProcessedInSameProcess();

  info("Removing all logins to test auto-saving of generated passwords");
  await LoginManager.removeAllUserFacingLogins();

  // form should not be filled
  checkForm(2, "", "");

  async function showAndSelectACPopupItem(index) {
    form.password.focus();
    if (form.password.value) {
      form.password.select();
      synthesizeKey("KEY_Backspace");
    }
    const autocompleteItems = await popupByArrowDown();
    if (index < 0) {
      index = autocompleteItems.length + index;
    }
    for (let i=0; i<=index; i++) {
      synthesizeKey("KEY_ArrowDown");
    }
    await TestUtils.waitForTick();
    return autocompleteItems[index];
  }

  let storagePromise, menuLabel, itemIndex, savedLogins;

  // fill the password field with the generated password via auto-complete menu
  storagePromise = promiseStorageChanged(["addLogin"]);
  // select last-but-2 item - the one before the footer
  menuLabel = await showAndSelectACPopupItem(-2);
  is(menuLabel, "Use a Securely Generated Password", "Check item label");
  synthesizeKey("KEY_Enter");
  info("waiting for the password field to be filled with the generated password");
  await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled");
  info("Wait for generated password to be added to storage");
  await storagePromise;

  savedLogins = await LoginManager.getAllLogins();
  is(savedLogins.length, 1, "Check saved logins count");

  form.uname.focus();
  await TestUtils.waitForTick();

  is(form.password.value.length, LoginTestUtils.generation.LENGTH, "Check password looks generated");
  const GENERATED_PASSWORD = form.password.value;

  info("clear the password field and delete the saved login using the AC menu")
  storagePromise = promiseStorageChanged(["removeLogin"]);

  itemIndex = 0;
  menuLabel = await showAndSelectACPopupItem(itemIndex);
  ok(menuLabel.includes("No username"), "Check first item is the auto-saved login");
  // Send delete to remove the auto-saved login from storage
  // On OS X, shift-backspace and shift-delete work, just delete does not.
  // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
  synthesizeKey("KEY_Delete", {shiftKey: true});
  await storagePromise;

  form.uname.focus();
  await TestUtils.waitForTick();

  savedLogins = await LoginManager.getAllLogins();
  is(savedLogins.length, 0, "Check saved logins count");

  info("Re-fill with the generated password");
  // select last-but-2 item - the one before the footer
  menuLabel = await showAndSelectACPopupItem(-2);
  is(menuLabel, "Use a Securely Generated Password", "Check item label");
  synthesizeKey("KEY_Enter");
  info("waiting for the password field to be filled with the generated password");
  await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled");

  form.uname.focus();
  await TestUtils.waitForTick();
  is(form.password.value, GENERATED_PASSWORD, "Generated password has not changed");
});

// add_task(async function test_passwordGenerationShownTelemetry() {
//   // Should only be recorded once per principal origin per session, but the cache is cleared each time ``initLogins`` is called.
//   await waitForTelemetryEventsCondition(events => {
//     return events.length == 3;
//   }, { process: "parent", filterProps: TelemetryFilterPropsShown }, "Expect 3 shown telemetry events");
// });
</script>
</pre>
</body>
</html>
