«Один из участников нашего комьюнити в Slack недавно спросил, можно ли использовать Playwright для поиска битых ссылок на своих сайтах. Конечно можно, и ответ на этот вопрос охватывает так много различных аспектов Playwright, что это отличный повод для новой статьи о функциях Playwright для комьюнити.
Итак, ссылки, ведущие в никуда.
Если вы предпочитаете смотреть , а не читать, вот вам Ютуб, а если вы здесь только чтобы скопировать и вставить код — вот он на GitHub.
Проблема 404
Чтобы обнаружить битые ссылки скриптом в Playwright, нам нужно сделать две вещи:
- Обнаружить и извлечь все ссылки на странице.
- Сделать запросы ко всем этим URL и получить их статус-код.
test(`A page has no 404s`, async ({ page }, testInfo) => { // Перейти на нужную страницу await page.goto("https://your-url-to-test.com") // 1. Собрать все URL в ссылках со страницы // 2. Оценить URL-адреса и проверить статус-код })
Теперь разберемся с этими двумя задачами, шаг за шагом.
Как извлечь атрибут href из всех ссылок на странице
Чтобы извлечь все эти значения href
, можно было бы вызвать локатор вроде page.getByRole('link')
или page.locator('a')
, перебрать все элементы ссылок, и получить доступ к атрибуту href
.
Но, к сожалению, этот подход не сработает, потому что вы не будете перебирать локаторы. Локаторы Playwright ленивые, медленные, и будут работать только в сочетании с действием или ассертом (утверждением).
// Определить локатор. const cta = page.getByRole('button', {name: "Click me"}) // Оценить локатор, зайти в DOM // и щелкнуть на полученном элементе. await cta.click() // Оценить локатор, зайти в DOM // и проверить, виден ли соответствующий элемент. await expect(cta).toBeVisible()
Именно благодаря такому поведению локатора мы можем полагаться на автоожидание Playwright при сквозном тестировании. Вы определяете целевой элемент «blueprint», и когда этот blueprint используется с действием или утверждением, Playwright будет постоянно запрашивать DOM и ждать совпадения элементов. Отлично!
Однако в нашем случае с извлечением ссылок такое «ленивое» поведение не поможет. Как сразу обратиться к DOM и оценить все ссылки?
locator.all() — сразу в DOM
Если вы выходите за рамки классического сквозного тестирования, как мы сейчас, вы можете вызвать метод locator.all
, чтобы обратиться к DOM и получить массив локаторов, соответствующих текущим элементам DOM.
Имея под рукой этот метод, мы можем оценить все целевые URL ссылок.
const links = page.locator(“a”)
// Оценить все доступные ссылки. const allLinks = await links.all() // Дождаться завершения всех вызовов getAttribute(). const allLinkHrefs = await Promise.all( // Извлечь атрибут `href`. allLinks.map((link) => link.getAttribute("href")) ) // ['https://example.com', '/something', '/something-else', ...]
После завершения определения локатора с помощью .all()
мы можем перебирать локаторы и сопоставлять их со значением атрибута с помощью getAttribute()
. Обратите внимание, что getAttribute()
— это асинхронная операция, возвращающая другой промис, поэтому мы должны обернуть все в Promise.all
, чтобы дождаться, пока все значения href
станут доступны.
С помощью этих нескольких строк мы извлекли все значения href
. Теперь мы могли бы увидеть, вернут ли эти URL при вызове нужный статус-код 200
, но давайте добавим больше возможностей в наше извлечение ссылок.
Сохранять запросы, удалять дубликаты ссылок
Когда мы оценили все ссылки, то велика вероятность, что в коллекции есть дубликаты. Например, ссылка на домашнюю (/
), скорее всего, будет в коллекции несколько раз. И хотя в этих дубликатах нет ничего страшного, зачем нам проверять целевой URL на наличие правильного кода состояния несколько раз?
Давайте удалим дубликаты, сделав ставку на собственный набор (сет) JavaScript. Наборы обладают замечательной особенностью — они хранят только уникальные значения. Если мы добавим одно и то же значение дважды, оно будет автоматически проигнорировано. Нам не нужно проверять, нет ли этого значения в наборе. Отлично!
А когда мы уже перебираем таргеты ссылок, мы также можем удалить mailto:
и якорные ссылки (#something
) в один прием!
// Преобразуем массив таргетов ссылок в набор, чтобы избежать дублирования. const validHrefs = allLinkHrefs.reduce((links, link) => { // Фильтруем ненужные ссылки href, `mailto:` и `#`. if (link && !link?.startsWith("mailto:") && !link?.startsWith("#")) links.add(link) return links }, new Set<string>())
С помощью этих строк мы удалили дубликаты, но заметили ли вы, что теперь мы отфильтровываем и ссылки, не содержащие истинного значения href? Причина в том, что ссылки могут содержать просто пустую строку (). Щелчок по таким ссылкам приводит только к перезагрузке страницы, и их также не должно быть на ваших страницах. Но если мы отфильтруем их, то не узнаем, есть ли на странице битые ссылки.
Давайте добавим мягкий ассерт в наш маппинг ссылок, чтобы получать уведомления о битых ссылках.
Мягкие утверждения Playwright: собрать ошибки, но продолжить выполнение
Всякий раз, когда вы используете ассерты Playwright с expect
, эти ассерты будут выбрасывать исключение и препятствовать выполнению вашего тест-кейса. Для сквозных тестов такое поведение имеет смысл. Когда вы нажимаете кнопку, ожидайте появления модала для заполнения включенной формы; если модал не появится, инструкции Playwright по заполнению формы также не сработают. Так зачем продолжать тест?
// Это исключение приведет к выбросу ошибки и остановке тест-кейса. await expect(headline).toBeVisible()
Но в нашем случае итерации и оценки ссылок мы не хотим выбрасывать упавшие утверждения и продолжать тест. Всякий раз, когда мы обнаруживаем недопустимый таргет ссылки (или плохой статус-код потом), мы хотим продолжить тест для проверки оставшихся URL и только в конце объявить весь тест упавшим. Как это сделать?
Для таких случаев в Playwright предусмотрены «мягкие» утверждения (expect.soft()
). Мягкие утверждения работают так же как и обычные, но они не выбрасывают уведомление при ошибке. Ошибки будут собираться и отображаться в конце тест-кейса.
const validHrefs = allLinkHrefs.reduce((links, link) => { // Проверить таргет ссылки, но не выбрасывать ошибку о неудачном утверждении. expect.soft(link).toBeTruthy() if (link && !link?.startsWith("mailto:") && !link?.startsWith("#")) links.add(link) return links }, new Set<string>())
Теперь мы можем собрать и все эти ошибки битых ссылок. Не хватает последней детали!
Нормализация локальных таргетов ссылок и гарантирование абсолютных URL
Когда мы извлечем все значения href
, то, скорее всего, обнаружим там локальные ссылки, такие как /
или /features
. Если мы захотим проверить статус-код полученных URL, мы не сможем запросить их, поскольку они требуют соответствующего протокола и домена.
Чтобы преобразовать относительные ссылки в абсолютные URL, мы можем использовать еще одну нативную фишку JavaScript — конструктор URL(). Я не буду вдаваться в подробности, но URL()
— это мощная вещь, стоящая за большинством операций с URL в JavaScript. Вы можете передать ему URL (неважно, относительный или абсолютный), базовый URL, и new URL()
сделает за вас весь парсинг URL. Очень круто.
new URL( "https://checklyhq.com", "https://example.com" ).href // 👆 "https://checklyhq.com" new URL( "/raccoon", "https://checklyhq.com" ).href // 👆 "https://checklyhq.com/raccoon" new URL( "/raccoon", "https://checklyhq.com/some-path" ).href // 👆 "https://checklyhq.com/raccoon"
Владея этим конструктором, мы можем переписать извлечение ссылок, чтобы убедиться, что все найденные целевые URL-адреса ссылок будут абсолютными.
const validHrefs = allLinkHrefs.reduce((links, link) => { expect.soft(link).toBeTruthy() if (link && !link?.startsWith("mailto:") && !link?.startsWith("#")) // Проверяем, что все URL-адреса являются абсолютными. links.add(new URL(link, page.url()).href) return links }, new Set<string>())
Вызвав new URL()
с извлеченной ссылкой и URL текущей страницы (page.url().href
), мы можем нормализовать все целевые ссылки.
И теперь мы готовы проверить, все ли URL возвращают правильный код состояния. Ссылка на полный код — в конце статьи.
Как проверить URL-адреса битых ссылок
Теперь, когда у нас есть набор, содержащий все URL-адреса, мы можем начать делать запросы и проверять «зеленые» коды состояния. Мы могли бы воспользоваться фикстурой Playwright для этих запросов, но, к счастью, соответствующий page object уже содержит объект request
.
Но в чем разница между ними? page.request
будет выполнять запросы в контексте текущей страницы. Например, если у вас есть тест-кейс, который выполняет логин пользователя, объект page
текущей страницы будет хранить некоторые сессионные куки. И если вы затем сделаете запрос с помощью page.request
, HTTP-вызов будет включать в себя и эти сессионные куки.
Если вы хотите сделать вызовы API от имени вошедшего пользователя, page.request
— это то, что нужно.
// Выполнить запрос, используя данные сессии текущей страницы. const response = await page.request.get(url) // Теперь мы можем перебирать все URLы запросов и проверять через if, возвращают ли все они зеленый статус-код. // Перебрать URL-адреса и проверить статус-код. for (const url of validHrefs) { try { const response = await page.request.get(url) expect .soft(response.ok(), `${url} has no green status code`) .toBeTruthy() } catch { expect.soft(null, `${url} has no green status code`).toBeTruthy() } }
Цикл включает в себя мягкие ассерты для продолжения работы в случае невыполнения условия, и мы добавили кастомные сообщения об ошибках, для упрощения отладки. Теперь вы можете запускать Playwright в CI/CD-конвейере и проверять битые ссылки при развертывании сайтов. Вот ссылка на код проекта. Удачи!»