Как проверить, отключены ли кнопки формы при нажатии кнопки отправки в Playwright

У меня есть фрагмент кода, который отключит форму при запуске процесса submit. Это классическое веб-приложение, а не одностраничное приложение, поэтому кнопка отправки фактически выходит за пределы страницы. Ниже приведен пример HTML того, как я это реализовал.

<!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>

Я хотел проверить, правильно ли работает приведенный выше код в Playwright, но я не могу заставить утверждение isDisabled работать в следующем коде, если раскомментирую утверждение.

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= = ");
});

Следующие были опробованы

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= = ");
});

Это также не работает: пример в предоставленном ответе, похоже, работает, пока вы на самом деле не попытаетесь проверить следующую страницу.

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= = ");
});

Я также пытался изменить route, но все равно не работает.

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

Почему бы просто не показать наложение с указателем-event: none?

mplungjan 06.07.2024 17:07

Можете ли вы поделиться полной страницей и кодом тестирования в качестве минимально воспроизводимого примера? Возможно, вы захотите начать прослушивать отключенное условие перед нажатием, в противном случае я предполагаю, что отправка формы происходит настолько быстро, что у вас не будет возможности ее уловить.

ggorlen 06.07.2024 17:35
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
2
102
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

После обновления OP, разъясняющего их вариант использования, в настоящее время это кажется невозможным, поскольку навигация немедленно делает элементы на странице недействительными, что делает невозможным их поиск или подтверждение их состояния.

Я оставлю свой первоначальный ответ ниже для контекста.


Предполагая, что отправка происходит быстро, я бы попробовал сначала прослушать отключенное состояние, а затем щелкнуть, чтобы вы не пропустили краткое состояние, которое возникает только на мгновение перед обновлением страницы:

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

Если обновление страницы происходит мгновенно, утверждение не будет выполнено, поскольку обновление страницы, вызванное вызовом .submit(), произойдет до того, как страница получит возможность отобразить отключенное состояние формы. Предполагая, что это достаточно точно относится к вашему варианту использования, вы можете поиграть со следующим примером, чтобы увидеть поведение:

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(),
  ]);
});

Поэтому, если отправка выполняется быстро, возможно, вы не сможете точно проверить это, если не считать небольшого инструментирования исходной страницы.

Альтернативная идея — устроить гонку expect(submitButton).toBeDisabled() против ожиданий навигации. Если ожидание навигации выполняется первым, а отключенная проверка не получает возможности выполниться, то считайте, что проверка также пройдена.

Что-то вроде:

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;
});

Недостатком этого подхода является то, что если навигация выходит из отключенного состояния, что было бы в обычных обстоятельствах без тайм-аута, то ваш тест повлечет за собой штраф в 1 секунду. Не ужасно, но немного медленнее, чем должно быть. Вы можете уменьшить таймаут из-за риска ложных срабатываний.

Если это не сработает, поделитесь полным, воспроизводимым примером с тестируемой страницей, чтобы я мог увидеть проблему своими глазами и поэкспериментировать.

Кажется, я не могу заставить его работать.

Archimedes Trajano 08.07.2024 18:33

Можете ли вы рассказать об этом подробнее? Что именно не работает? Мой пример работает; Можете ли вы предоставить неудачный пример реальной страницы, чтобы у нас обоих был общий контекст?

ggorlen 08.07.2024 18:34

Я добавил приведенный выше пример, но не могу заставить «отключено» сработать. Самое сложное — получить доступную для тестирования серверную службу, способную выполнять задержку.

Archimedes Trajano 08.07.2024 18:42

ваш второй пример не проверяет полученный результат, поэтому, хотя он и проверяет «отключено», навигация по сообщениям не проходит.

Archimedes Trajano 08.07.2024 19:08
setTimeout(() => form.submit(), 100); имитирует медленный бэкэнд. Отрегулируйте соответствующим образом. В вашем expect(page).toHaveURL("https://httpbun.com/mix/s=200/d=3/b6‌​4=dGVzdA= = "); отсутствует await.
ggorlen 08.07.2024 20:20

Тем не менее, я вижу await expect(submitButton).toBeDisabled(); неудачу, вероятно, потому, что как только начинается навигация, страница немедленно удаляет свой предыдущий контекст, хотя он все еще отображается на экране. Так что да, я не знаю, как это проверить, но я изучу это подробнее, если у меня будет возможность.

ggorlen 08.07.2024 20:29

Я пришел к такому же выводу. Тогда откройте это github.com/microsoft/playwright/issues/31596

Archimedes Trajano 08.07.2024 20:40

В настоящее время Playwright, похоже, не поддерживает эту возможность.

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

Навигация начинается с изменения URL-адреса страницы или взаимодействия со страницей (например, щелчка ссылки). Цель навигации может быть отменена, например, при попадании на неразрешенный адрес DNS или преобразована в загрузку файла.

После запуска навигации элементы становятся недоступными до тех пор, пока страница не загрузится даже с помощью noWaitAfter: true.

Открыл этот запрос функции, чтобы, надеюсь, решить проблему https://github.com/microsoft/playwright/issues/31596

Ответ принят как подходящий

В конце концов я сделал обходной путь из-за исчезновения DOM при запуске навигации. Я заменил form.submit фиктивным методом, чтобы гарантировать, что он не перемещается, а затем вызвал его впоследствии. Следует отметить одну вещь: вы не можете установить локальные переменные в evaluate, поэтому вам нужно применить их как свойство в контексте.

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.info("form is submitting");
            event.preventDefault();
            disableForm(aForm);
            aForm.submit();
            console.info("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 = "[email protected]"><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");
});

Другие вопросы по теме