<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Don't send onFormSubmit message on navigation if the user did not interact
    with the login fields</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="pwmgr_common.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>

<div id="content">
  <iframe id="loginFrame">
  </iframe>
</div>

<pre id="test"></pre>
<script>
const { TestUtils } = SpecialPowers.ChromeUtils.import(
  "resource://testing-common/TestUtils.jsm"
);
SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popup to show");

const EXAMPLE_COM = window.location.origin + "/tests/toolkit/components/passwordmgr/test/mochitest/";
const PREFILLED_FORM_URL = EXAMPLE_COM + "subtst_prefilled_form.html"

let iframe = document.getElementById("loginFrame");

function waitForLoad() {
  return new Promise(resolve => {
    function handleLoad() {
      iframe.removeEventListener("load", handleLoad);
      resolve();
    }
    iframe.addEventListener("load", handleLoad);
  });
}

async function setupWithOneLogin(pageUrl) {
  let origin = window.location.origin;
  await setStoredLoginsAsync([origin, origin, null, "user1", "pass1"]);

  let chromeScript = runInParent(async function testSetup() {
    let logins = await Services.logins.getAllLogins();
    for (let l of logins) {
      info("Got login: " + l.username + ", " + l.password);
    }
  });

  await setup(pageUrl);
  return chromeScript;
}

function resetSavedLogins() {
  let chromeScript = runInParent(function testTeardown() {
    Services.logins.removeAllUserFacingLogins();
  });
  chromeScript.destroy();
}

async function setup(pageUrl) {
  let loadPromise = waitForLoad();
  let processedFormPromise = promiseFormsProcessed();
  iframe.src = pageUrl;

  await processedFormPromise;
  info("initial form processed");
  await loadPromise;
  await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
    let doc = this.content.document;
    let link = doc.createElement("a");
    link.setAttribute("href", "http://mochi.test:8888");
    doc.body.appendChild(link);
  });
}

async function navigateWithoutUserInteraction() {
  let loadPromise = waitForLoad();
  await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
    let doc = this.content.document;
    let hadInteracted = doc.userHasInteracted;
    let target = doc.querySelector("a[href]");
    if (target) {
      target.click();
    } else {
      target = doc.querySelector("form");
      target.submit();
    }
    is(doc.userHasInteracted, hadInteracted, "document.userHasInteracted shouldn't have changed");
  });
  await loadPromise;
}

async function userInput(selector, value) {
  await SpecialPowers.spawn(getIframeBrowsingContext(window), [selector, value], async function(sel, val) {
    // use "real" synthesized events rather than setUserInput to ensure
    // document.userHasInteracted is flipped true
    let EventUtils = ContentTaskUtils.getEventUtils(content);
    let target = this.content.document.querySelector(sel);
    target.focus();
    target.select();
    await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content);
    await EventUtils.sendString(val, this.content);
    info(
      `userInput: new target.value: ${target.value}`
    );
    target.blur();
    return Promise.resolve();
  });
}

function checkDocumentUserHasInteracted() {
  return SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
    return this.content.document.userHasInteracted;
  });
}

add_task(async function test_init() {
  // For this test, we'll be testing with & without user document interaction.
  // So we'll reset the pref which dictates the behavior of
  // LoginFormState._formHasModifiedFields in automation
  // and ensure all interactions are properly emulated
  ok(SpecialPowers.getBoolPref("signon.testOnlyUserHasInteractedByPrefValue"), "signon.testOnlyUserHasInteractedByPrefValue should default to true");
  info("test_init, flipping the signon.testOnlyUserHasInteractedByPrefValue pref");
  await SpecialPowers.pushPrefEnv({"set": [
    ["signon.testOnlyUserHasInteractedByPrefValue", false],
  ]});
  SimpleTest.registerCleanupFunction(async function cleanup_pref() {
    await SpecialPowers.popPrefEnv();
  });

  await SimpleTest.promiseWaitForCondition(() => LoginHelper.testOnlyUserHasInteractedWithDocument === null);
  is(LoginHelper.testOnlyUserHasInteractedWithDocument, null,
    "LoginHelper.testOnlyUserHasInteractedWithDocument should be null for this set of tests");
});

add_task(async function test_no_message_on_navigation() {
  // If login field values were set by the website, we don't message to save the
  // login values if the user did not interact with the fields before submiting.
  await setup(PREFILLED_FORM_URL);

  let submitMessageSent = false;
  getSubmitMessage().then(value => {
    submitMessageSent = true;
  });
  await navigateWithoutUserInteraction();

  // allow time to pass before concluding no onFormSubmit message was sent
  await new Promise(res => setTimeout(res, 1000));
  ok(!submitMessageSent, "onFormSubmit message is not sent on navigation since the login fields were not modified");
});

add_task(async function test_prefd_off_message_on_navigation() {
  // Confirm the pref controls capture behavior with non-user-set field values.
  await SpecialPowers.pushPrefEnv({"set": [
    ["signon.userInputRequiredToCapture.enabled", false],
  ]});
  await setup(PREFILLED_FORM_URL);

  let promiseSubmitMessage = getSubmitMessage();
  await navigateWithoutUserInteraction();
  await promiseSubmitMessage;
  info("onFormSubmit message was sent as expected after navigation");

  SpecialPowers.popPrefEnv();
});

add_task(async function test_message_with_user_interaction_on_navigation() {
  await setup(PREFILLED_FORM_URL);
  await userInput("#form-basic-username", "foo");

  let promiseSubmitMessage = getSubmitMessage();
  await navigateWithoutUserInteraction();
  await promiseSubmitMessage;
  info("onFormSubmit message was sent as expected after user interaction");
});

add_task(async function test_empty_form_with_input_handler() {
  await setup(EXAMPLE_COM + "formless_basic.html");
  await userInput("#form-basic-username", "user");
  await userInput("#form-basic-password", "pass");

  let promiseSubmitMessage = getSubmitMessage();
  await navigateWithoutUserInteraction();
  await promiseSubmitMessage;
  info("onFormSubmit message was sent as expected after user interaction");
});

add_task(async function test_no_message_on_autofill_without_user_interaction() {
  let chromeScript = await setupWithOneLogin(EXAMPLE_COM + "form_basic.html");
  // Check for autofilled values.
  await checkLoginFormInFrame(getIframeBrowsingContext(window, 0),
                                   "form-basic-username", "user1",
                                   "form-basic-password", "pass1");

  info("LoginHelper.testOnlyUserHasInteractedWithDocument:" +
      LoginHelper.testOnlyUserHasInteractedWithDocument
  );
  ok(!(await checkDocumentUserHasInteracted()), "document.userHasInteracted should be initially false");
  let submitMessageSent = false;
  getSubmitMessage().then(value => {
    submitMessageSent = true;
  });
  info("Navigating the page")
  await navigateWithoutUserInteraction();

  // allow time to pass before concluding no onFormSubmit message was sent
  await new Promise(res => setTimeout(res, 1000));

  chromeScript.destroy();
  resetSavedLogins();

  ok(!submitMessageSent, "onFormSubmit message is not sent on navigation since the document had no user interaction");
});

add_task(async function test_message_on_autofill_with_document_interaction() {
  // We expect that as long as the form values !== their defaultValues,
  // any document interaction allows the submit message to be sent

  let chromeScript = await setupWithOneLogin(EXAMPLE_COM + "form_basic.html");
  // Check for autofilled values.
  await checkLoginFormInFrame(getIframeBrowsingContext(window, 0),
                                   "form-basic-username", "user1",
                                   "form-basic-password", "pass1");

  let userInteracted = await checkDocumentUserHasInteracted();
  ok(!userInteracted, "document.userHasInteracted should be initially false");

  await SpecialPowers.spawn(getIframeBrowsingContext(window), ["#form-basic-username"], async function(sel) {
    // Click somewhere in the document to ensure document.userHasInteracted is flipped to true
    let EventUtils = ContentTaskUtils.getEventUtils(content);
    let target = this.content.document.querySelector(sel);

    await EventUtils.synthesizeMouseAtCenter(target, {}, this.content);
  });

  userInteracted = await checkDocumentUserHasInteracted();
  ok(userInteracted, "After synthesizeMouseAtCenter, document.userHasInteracted should be true");

  let promiseSubmitMessage = getSubmitMessage();
  await navigateWithoutUserInteraction();

  let { data } = await promiseSubmitMessage;
  ok(data.autoFilledLoginGuid, "Message was sent with autoFilledLoginGuid");
  info("Message was sent as expected after document user interaction");

  chromeScript.destroy();
  resetSavedLogins();
});

add_task(async function test_message_on_autofill_with_user_interaction() {
  // Editing a field value causes the submit message to be sent as
  // there is both document interaction and field modification
  let chromeScript = await setupWithOneLogin(EXAMPLE_COM + "form_basic.html");
  // Check for autofilled values.
  await checkLoginFormInFrame(getIframeBrowsingContext(window, 0),
                                   "form-basic-username", "user1",
                                   "form-basic-password", "pass1");

  userInput("#form-basic-username", "newuser");
  let promiseSubmitMessage = getSubmitMessage();
  await navigateWithoutUserInteraction();

  let { data } = await promiseSubmitMessage;
  ok(data.autoFilledLoginGuid, "Message was sent with autoFilledLoginGuid");
  is(data.usernameField.value, "newuser", "Message was sent with correct usernameField.value");
  info("Message was sent as expected after user form interaction");

  chromeScript.destroy();
  resetSavedLogins();
});

add_task(async function test_no_message_on_user_input_from_other_form() {
  // ensure input into unrelated fields on the page don't change login form modified-ness
  await setup(PREFILLED_FORM_URL);

  // Add a form which will not be submitted and an input associated with that form
  await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() {
    let doc = this.content.document;
    let loginForm = doc.querySelector("form");
    let fragment = doc.createDocumentFragment();
    let otherForm = doc.createElement("form");
    otherForm.id ="otherForm";
    fragment.appendChild(otherForm);

    let alienField = doc.createElement("input");
    alienField.id = "alienField";
    alienField.type = "text"; // not a password field
    alienField.setAttribute("form", "otherForm");
    // new field is child of the login, but a member of different non-login form via its .form property
    loginForm.appendChild(alienField);
    doc.body.appendChild(fragment);
  });
  await userInput("#alienField", "something");

  let submitMessageSent = false;
  getSubmitMessage().then(data => {
    info("submit mesage data: " + JSON.stringify(data));
    submitMessageSent = true;
  });

  info("submitting the form");
  await navigateWithoutUserInteraction();

  // allow time to pass before concluding no onFormSubmit message was sent
  await new Promise(res => setTimeout(res, 1000));
  ok(!submitMessageSent, "onFormSubmit message is not sent on navigation since no login fields were modified");
});

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