skip to Main Content

I have a bit of code that would disable a form when I start the submit process. This is an classic web app not a Single Page app so the submit button will actually navigate out of the page. The following is an example HTML of how I implemented this.

<!DOCTYPE html>
<html lang="en">

<head>
  <title>MVCE</title>
  <script>
    function disableForm(theForm) {
      theForm.querySelectorAll("input, textarea, select").forEach(
        /** @param {HTMLInputElement} element */
        (element) => {
          element.readOnly = true;
        }
      );
      theForm
        .querySelectorAll("input[type='submit'], input[type='button'], button")
        .forEach(
          /** @param {HTMLButtonElement} element */
          (element) => {
            element.disabled = true;
          }
        );
    }

    document.addEventListener('DOMContentLoaded',
      function applyDisableFormOnSubmit(formId) {
        document.querySelectorAll("form").forEach((aForm) => {
          aForm.addEventListener("submit", (event) => {
            event.preventDefault();
            disableForm(aForm);
            aForm.submit();
          });
        });
      });

  </script>
</head>

<body>
  <form class="generated-form" id="x" method="POST" action="https://httpbun.com/mix/s=200/d=3/b64=dGVzdA==">
    <fieldset>
      <legend> Student:: </legend>
      <label for="fname">First name:</label><br>
      <input type="text" id="fname" name="fname" value="John"><br>
      <label for="lname">Last name:</label><br>
      <input type="text" id="lname" name="lname" value="Doe"><br>
      <label for="email">Email:</label><br>
      <input type="email" id="email" name="email" value="[email protected]"><br><br>
      <input type="submit" value="Submit">
    </fieldset>
  </form>
</body>

</html>

I wanted to test the above code is functioning correctly in Playwright, but I cannot get the isDisabled assertion to work in the following code if I uncomment the assertion.

test("disable form", async ({ page }) => {
  await page.goto("http://localhost:8080/");
  const submitButton = page.locator("input[type='submit']");
  await expect(submitButton).toBeEnabled();
  await submitButton.click();
  // await expect(submitButton).toBeDisabled();
  await expect(page)
    .toHaveURL("https://httpbun.com/mix/s=200/d=3/b64=dGVzdA==");
});

The following have been tried

test("disable form", async ({ page }) => {
  await page.goto("http://localhost:8080/");
  const submitButton = page.locator("input[type='submit']");
  await expect(submitButton).toBeEnabled();
  await Promise.all([
    submitButton.click(),
    expect(submitButton).toBeDisabled()
  ]);
  expect(page).toHaveURL("https://httpbun.com/mix/s=200/d=3/b64=dGVzdA==");
});

This also does not work, the example in the answer provided appears to be working until you actually try to verify the following page.

test("disable form", async ({ page }) => {
  await page.goto("http://localhost:8080/");
  const submitButton = page.locator("input[type='submit']");

  await expect(submitButton).toBeEnabled();
  const nav = page.waitForNavigation({timeout: 5000});
  const disabled = expect(submitButton).toBeDisabled({timeout: 1000});
  await submitButton.click();

  // allow disabled check to pass if the nav happens
  // too fast and the button disabled state doesn't render
  await disabled.catch(() => {});
  await nav;

  expect(page).toHaveURL("https://httpbun.com/mix/s=200/d=3/b64=dGVzdA==");
});

2

Answers


  1. Chosen as BEST ANSWER

    At present Playwright does not appear to support this capability as per

    per https://playwright.dev/docs/navigations#navigation-events

    Navigation starts by changing the page URL or by interacting with the page (e.g., clicking a link). The navigation intent may be canceled, for example, on hitting an unresolved DNS address or transformed into a file download.

    Once navigation starts the elements are no longer accessible until the page is loaded even with noWaitAfter: true

    Opened this feature request to hopefully solve the issue https://github.com/microsoft/playwright/issues/31596


  2. Assuming the submission is fast, I’d try listening for the disabled condition first, then clicking, so you won’t miss the brief state that occurs only for an instant before page refresh:

    const submitButton = page.locator("#SubmitButton")
    await expect(submitButton).toBeEnabled();
    await Promise.all([
      expect(submitButton).toBeDisabled(),
      submitButton.click(),
    ]);
    

    If the page refresh happens instantly, the assertion will fail because the page refresh caused by the .submit() call will occur before the page gets a chance to render the disabled form state. Assuming it pertains accurately enough to your use case, you can play with the following example to see the behavior:

    import {expect, test} from "@playwright/test"; // ^1.42.1
    
    const html = `<!DOCTYPE html><html><body>
    <form><input id="SubmitButton" type="submit"></form>
    <script>
    const form = document.querySelector("form");
    const disableFormFields = (event) => {
      event.preventDefault();
       form
        .querySelectorAll("input, textarea, select")
        .forEach((element) => {
          element.readOnly = true;
        });
      form
        .querySelectorAll("input[type='submit'], input[type='button'], button")
        .forEach((element) => {
          element.disabled = true;
        });
    
      // Added for experimentation purposes, feel free to adjust or remove
      setTimeout(() => form.submit(), 100);
    };
    form.addEventListener("submit", disableFormFields);
    </script></body></html>`;
    
    test("button is disabled during submit", async ({page}) => {
      await page.setContent(html);
      const submitButton = page.locator("#SubmitButton");
      await expect(submitButton).toBeEnabled();
      await Promise.all([
        expect(submitButton).toBeDisabled(),
        submitButton.click(),
      ]);
    });
    

    So if the submission is fast, you may not be able to accurately test this, short of instrumenting the source page a bit.

    An alternative idea is to make the expect(submitButton).toBeDisabled() race against a navigation expectation. If the nav expectation runs first and the disabled check never gets a chance to run, then consider that test a pass too.

    Something like:

    test("button is disabled during submit", async ({page}) => {
      await page.setContent(html);
      const submitButton = page.locator("#SubmitButton");
      await expect(submitButton).toBeEnabled();
      const nav = page.waitForNavigation({timeout: 5000});
      const disabled = expect(submitButton).toBeDisabled({timeout: 1000});
      await submitButton.click();
    
      // allow disabled check to pass if the nav happens
      // too fast and the button disabled state doesn't render
      await disabled.catch(() => {});
      await nav;
    });
    

    One downside of this approach is that if the nav beats the disabled state, which it would under normal circumstances without the timeout, then your test will incur the 1 second penalty. Not terrible, but a bit slower than it should be. You can lower the timeout at the risk of false positives.

    If this doesn’t work, please share a complete, reproducible example with the page under test so I can see the issue first hand and experiment.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search