javascripttestingplaywright

How do I verify if form buttons are disabled when pressing the submit button in Playwright


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="youremail@gmail.com"><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==");
});

I have also tried to modify the route but it still does not work

  await page.route(
    "https://httpbun.com/mix/s=200/d=3/b64=dGVzdA==",
    async (route, req) => {
      console.log("routing")
      expect(page.url()).toEqual(currentUrl);
      await expect(submitButton).toBeVisible();
      await expect(submitButton).toBeDisabled();
      return route.continue();
    }
  );

Solution

  • I eventually did a round about way due to the DOM disappearing when navigation starts. What I did was replace the form.submit with a dummy method to ensure it doesn't navigate and then invoke it afterwards. One thing to note is you cannot set the local variables in the evaluate so you need to apply it as an property on the context.

    type HTMLFormElementWithFlags = HTMLFormElement & {
      formSubmitCalled: boolean;
      /**
       * Stores the form.submit function to be called later
       */
      savedSubmit: () => void;
    };
    /**
     *
     * @param formLocator form locator
     * @returns [ submitTrappedForm, isTrappedFormSubmitCalled ]
     */
    const trapFormSubmit = async (
      formLocator: Locator
    ): Promise<[() => Promise<void>, () => Promise<boolean>]> => {
      await formLocator.evaluate((elem: HTMLFormElementWithFlags) => {
        elem.savedSubmit = elem.submit;
        elem.formSubmitCalled = false;
        elem.submit = () => {
          elem.formSubmitCalled = true;
        };
      });
      return [
        async () =>
          formLocator.evaluate((elem: HTMLFormElementWithFlags) => {
            elem.savedSubmit();
          }),
        async () =>
          formLocator.evaluate(
            (elem: HTMLFormElementWithFlags) => elem.formSubmitCalled
          ),
      ];
    };
    
    test("disable form on submit", async ({ page }) => {
      await page.setContent(
        `
    <!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) => {
                console.log("form is submitting");
                event.preventDefault();
                disableForm(aForm);
                aForm.submit();
                console.log("submit is called");
              });
            });
          });
    
      </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="youremail@gmail.com"><br><br>
          <input type="submit" value="Submit">
        </fieldset>
      </form>
    </body>
    
    </html>
    `
      );
      const submitButton = page.locator("input[type='submit']");
    
      await expect(submitButton).toBeEnabled();
      const currentUrl = page.url();
    
      const form = page.locator("form");
    
      const [submitTrappedForm, isTrappedFormSubmitCalled] =
        await trapFormSubmit(form);
      await submitButton.click();
      expect(page.url()).toEqual(currentUrl);
      expect(await isTrappedFormSubmitCalled()).toBe(true);
      await expect(submitButton).toBeVisible();
      await expect(submitButton).toBeDisabled();
      await submitTrappedForm();
    
      await page.waitForURL("https://httpbun.com/mix/s=200/d=3/b64=dGVzdA==");
      await expect(page).toHaveURL(
        "https://httpbun.com/mix/s=200/d=3/b64=dGVzdA=="
      );
      const content = await page.content();
      // Cannot use toEqual because playwright wraps it as an HTML.
      expect(content).toContain("test");
    });