<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Test filling generated passwords into confirm password fields</title>
  <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: filling generated passwords into confirm password fields

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

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

  <!-- form1 has new-password followed by confirm-password fields -->
  <form id="form1" action="https://example.com" onsubmit="return false;">
    <input type="text" name="uname">
    <input type="password" name="pword" autocomplete="new-password">
    <input type="password" name="pword-next">
    <button type="submit">Submit</button>
  </form>

  <!-- form2 has 2 new-password fields -->
  <form id="form2" action="https://example.com" onsubmit="return false;">
    <input type="text" name="uname">
    <input type="password" name="pword" autocomplete="new-password">
    <input type="password" name="pword-between">
    <input type="password" name="pword-next" autocomplete="new-password">
    <button type="submit">Submit</button>
  </form>

  <!-- form3 has lots of junk fields before the confirm-password field -->
  <form id="form3" action="https://example.com" onsubmit="return false;">
    <input type="text" name="uname">
    <input type="password" name="pword" autocomplete="new-password">
    <input type="text" name="junk0">
    <input type="text" name="junk1">
    <input type="text" name="junk2">
    <input type="text" name="junk3">
    <input type="text" name="junk4">
    <input type="password" name="pword-next">
    <button type="submit">Submit</button>
  </form>

  <!-- form4 has a password field after the confirm-password field we don't want to fill into -->
  <form id="form4" action="https://example.com" onsubmit="return false;">
    <input type="text" name="uname">
    <input type="password" name="pword" autocomplete="new-password">
    <input type="password" name="pword-next" autocomplete="new-password">
    <input type="password" name="pword-extra" autocomplete="new-password">
    <button type="submit">Submit</button>
  </form>
</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"
);

const setupScript = runInParent(function parentTestSetup() {
  const { LoginTestUtils } = ChromeUtils.import(
    "resource://testing-common/LoginTestUtils.jsm"
  );

  addMessageListener(
    "resetLoginsAndGeneratedPasswords", () => {
      LoginTestUtils.clearData();
      LoginTestUtils.resetGeneratedPasswordsCache();
    }
  );
});

function testReset() {
  return setupScript.sendAsyncMessage("resetLoginsAndGeneratedPasswords");
}

function generateDateString(date) {
  let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined,
                                                              { dateStyle: "medium" });
  return dateAndTimeFormatter.format(date);
}

const DATE_NOW_STRING = generateDateString(new Date());

async function fillWithGeneratedPassword(input, expectedResults = ["Use a Securely Generated Password"]) {
  info("Opening the AC menu");
  const { items } = await openPopupOn(input);
  checkAutoCompleteResults(items, expectedResults,
    window.location.host, "Check all rows are correct");

  for (let autocompleteItem of items) {
    synthesizeKey("KEY_ArrowDown");
    if (autocompleteItem == "Use a Securely Generated Password") {
      synthesizeKey("KEY_Enter");
      await untilAutocompletePopupClosed();
      break;
    }
  }
}

async function testConfirmPasswordFieldFilledWithGeneratedPassword({
  formNumber,
  passwordInputName = "pword",
  confirmInputName = "pword-next",
  expectedACResults,
  expectedFilled = [],  // the names of the inputs which should be filled
  // field-name: expected-value for any non-filled inputs which should have values other than their .defaultValue
  expectedNonDefaultValues = {},
  expectedMasked = [],  // the names of the inputs which should be masked
  beforeFn,
  afterFn
}) {
  // form should not be initially filled
  checkForm(formNumber, "", "");

  if (beforeFn) {
    await beforeFn(document.getElementById(`form${formNumber}`));
    document.documentElement.scrollTop;  // Flush pending reflows which may be caused by beforeFn.
  }
  let pword = getFormElementByName(formNumber, passwordInputName || "pword");
  let pword2 = getFormElementByName(formNumber, confirmInputName || "pword-next");

  await fillWithGeneratedPassword(pword, expectedACResults);

  info("waiting for the password field to be filled with the generated password");
  await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");

  let generatedPW = pword.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(pword, false, "After fill");

  info("Check the expected password fields are filled");
  for (let input of pword.form.querySelectorAll("input")) {
    // check field filling & highlights
    if (expectedFilled.includes(input.name)) {
      await ContentTaskUtils.waitForCondition(() => {
        return input.matches(":autofill");
      }, `Highlight was successfully applied to the (${input.name}) field`);

      is(input.value, generatedPW, `Field (${input.name}) has the generated password value`);
    } else {
      await ContentTaskUtils.waitForCondition(() => {
        return !input.matches(":autofill");
      }, `Highlight was not applied to field (${input.name})`);

      let expectedValue = (input.name in expectedNonDefaultValues) ? expectedNonDefaultValues[input.name] : input.defaultValue;
      is(input.value, expectedValue, `Field (${input.name}) field has the expected value`);
    }

    if (expectedMasked.includes(input.name)) {
      LOGIN_FIELD_UTILS.checkPasswordMasked(input, true, `Field (${input.name}) should be masked`);
    }
  }

  if (expectedFilled.includes(pword2.name)) {
    info("Check the 2 field values aren't mirrored");
    // changing the password field value should result in a message sent to the parent process
    let messageSentPromise = getPasswordEditedMessage();

    pword.focus();
    // add a character. We don't expect the confirm password field to get the same change
    synthesizeKey("KEY_End");
    synthesizeKey("@");
    is(pword.value.length, GENERATED_PASSWORD_LENGTH + 1, "Value of the first password field changed");

    await TestUtils.waitForTick();
    is(pword2.value, generatedPW, "Value of the confirm field did not change");

    // focusing the 2nd password field blurs the first and results in a "change" event
    pword2.focus();
    info("Waiting for edit message");
    await messageSentPromise;

    LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Generated password is masked when blurred");
    if (expectedMasked.includes(pword2.name)) {
      LOGIN_FIELD_UTILS.checkPasswordMasked(pword2, false, "Confirm password should be be unmasked");
    }

    // remove a character from the confirm field.
    // We don't expect the filled password field to get the same change
    synthesizeKey("KEY_End");
    synthesizeKey("KEY_Backspace");
    is(pword2.value, generatedPW.substring(0, GENERATED_PASSWORD_LENGTH - 1), "Value of the confirm field changed");

    await TestUtils.waitForTick();
    is(pword.value.length, GENERATED_PASSWORD_LENGTH + 1, "Value of the first password field didn't change");

    // the confirm field has its highlight cleared when emptied
    pword2.focus();
    pword2.select();
    synthesizeKey("KEY_Backspace");
    await ContentTaskUtils.waitForCondition(() => {
        return !pword2.matches(":autofill");
    }, "Highlight was successfully cleared from the confirm-password field");

    // if it got originally masked (i.e. was a password field) verify the focused confirm field now masks
    // its input like a normal, non-generated password field after being emptied
    if (expectedMasked.includes(pword2.name)) {
      pword2.focus();
      synthesizeKey("a");
      LOGIN_FIELD_UTILS.checkPasswordMasked(pword2, true, "Confirm password gets masked again when focused");
    }
  } else {
    let expectedValue = (pword2.name in expectedNonDefaultValues) ? expectedNonDefaultValues[pword2.name] : pword2.defaultValue;
    is(pword2.value, expectedValue, "Value of the confirm-password field is unchanged");

    if (pword2.type == "password" && !pword2.disabled && !pword2.readOnly) {
      // make sure we didn't change the behavior of a non-generated password field
      // it should remain masked when it has focus
      pword2.focus();
      LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Non-generataed password field remains masked when focused");
    }
  }

  if (afterFn) {
    await afterFn(document.getElementById(`form${formNumber}`));
    document.documentElement.scrollTop;  // Flush pending reflows which may be caused by afterFn.
  }

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

  recreateTree(document.getElementById(`form${formNumber}`));
}

add_task(async function test_fillNextPasswordField() {
  const formNumber = 1;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    // the next password field should be filled with the generated password
    expectedFilled: ["pword", "pword-next"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    // the confirm field should be masked, the filled field has focus and should not be masked
    expectedMasked: ["pword-next"],
  });
});

add_task(async function test_fillNextPasswordFieldWasPasswordType() {
  const formNumber = 1;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    beforeFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      is(pword2.type, "password", "confirm field is of password type");
      pword2.type = "text";
    },
    // the next hasBeenTypePassword field should be filled with the generated password
    expectedFilled: ["pword", "pword-next"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    // the confirm field should is currently type=text so will not be masked
    expectedMasked: [],
    afterFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.type = "password";
    }
  });
});

add_task(async function test_dontFillNonEmptyPasswordField() {
  const formNumber = 1;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    beforeFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.setAttribute("value", "previous value");
    },
    // the would-be confirm field is not empty so will not be filled
    expectedFilled: ["pword"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
    expectedMasked: [],
    afterFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.setAttribute("value", "");
    }
  });
});

add_task(async function test_dontFillEditedNewPasswordField() {
  const formNumber = 2;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    beforeFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.focus()
      sendString("edited value");
    },
    // the would-be confirm field is not empty so will not be filled
    expectedFilled: ["pword"],
    expectedNonDefaultValues: {
      "pword-next": "edited value",
    },
    // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
    expectedMasked: [],
    afterFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.setAttribute("value", "");
    }
  });
});

add_task(async function test_ignoreReadOnlyField() {
  const formNumber = 1;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    beforeFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.readOnly = true;
    },
    // the confirm field candidate is read-only so will not be filled
    expectedFilled: ["pword"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
    expectedMasked: [],
    afterFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.readOnly = false;
    }
  });
});

add_task(async function test_ignoreDisabledField() {
  const formNumber = 1;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    beforeFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.disabled = true;
    },
    // the confirm field candidate is read-only so will not be filled
    expectedFilled: ["pword"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
    expectedMasked: [],
    afterFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.disabled = false;
    }
  });
});

add_task(async function test_preferMatchingAutoCompleteInfoPasswordField() {
  const formNumber = 2;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    expectedFilled: ["pword", "pword-next"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    expectedMasked: ["pword-next"],
  });
});

add_task(async function test_ignoreDisabledMatchingAutoCompleteInfoPasswordField() {
  const formNumber = 2;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    beforeFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.disabled = true;
    },
    // it should fill the next password field, not the disabled one
    confirmInputName: "pword-between",
    expectedFilled: ["pword", "pword-between"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    expectedMasked: ["pword-between"],
    afterFn(form) {
      let pword2 = form.querySelector("input[name='pword-next']");
      pword2.disabled = false;
    },
  });
});

add_task(async function test_ignoreTooDistantPasswordField() {
  const formNumber = 3;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    expectedFilled: ["pword"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    expectedMasked: [],
  });
});

add_task(async function test_tooManyDisabledFields() {
  // we don't fill into disabled fields,
  // but they do count towards the distance from the first password field
  const formNumber = 3;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    beforeFn(form) {
      for(let inp of form.querySelectorAll("input[name*='junk']")) {
        inp.disabled = true;
      }
    },
    expectedFilled: ["pword"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    expectedMasked: [],
    afterFn(form) {
      for(let inp of form.querySelectorAll("input[name*='junk']")) {
        inp.disabled = false;
      }
    },
  });
});

add_task(async function test_skipOverHiddenFields() {
  // hidden fields do not count towards the distance from the first password field
  const formNumber = 3;
  await testReset();
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    beforeFn(form) {
      for(let inp of form.querySelectorAll("input[name*='junk']")) {
        inp.type = "hidden";
      }
    },
    expectedFilled: ["pword", "pword-next"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    expectedMasked: ["pword-next"],
    afterFn(form) {
      for(let inp of form.querySelectorAll("input[name*='junk']")) {
        inp.type = "text";
      }
    },
  });
});

add_task(async function test_dontFill3rdPasswordField() {
  // if a generated password field was previously filled on a form
  // don't look for a confirm-password field when filling another field with a generated password
  const formNumber = 4;
  await testReset();

  let pword = getFormElementByName(formNumber, "pword");
  let pword2 = getFormElementByName(formNumber, "pword-next");
  let pword3 = getFormElementByName(formNumber, "pword-extra");
  await testConfirmPasswordFieldFilledWithGeneratedPassword({
    formNumber,
    async beforeFn(form) {
      // disable the following password fields so they dont get filled just yet
      pword2.disabled = true;
      pword3.disabled = true;

      info("beforeFn, filling the pword field");
      await fillWithGeneratedPassword(pword);
      info("beforeFn: waiting for the password field to be filled with the generated password");
      await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");

      pword2.disabled = false;
      pword3.disabled = false;
      is(pword2.value, "", "The pword-next field was not filled");
      is(pword3.value, "", "The pword-extra field was not filled");
    },
    // Fill into the confirm-password field,
    // we want to confirm this doesnt now try and fill another nearby "confirm" field
    passwordInputName: "pword-next",
    confirmInputName: "pword-extra",
    // we already generated a password in this test, so the AC menu will have the auto-saved login
    expectedACResults: [
      "No username (" + DATE_NOW_STRING + ")",
      "Use a Securely Generated Password",
    ],
    // we've manually filled both fields,
    // we don't expect the third 'pword-extr' field to be filled
    expectedFilled: ["pword", "pword-next"],
    // all fields should either be filled or have their .defaultValue
    expectedNonDefaultValues: {},
    expectedMasked: [
      "pword",     // pword was filled in the beforeFn and is blurred so it should be masked
      // pword-next was filled by the test and is focused so should not be masked
    ],
  });
});

</script>
</pre>
</body>
</html>
