<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 20:19:32 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="fT-oJ_pa7jJ4YVZwhc9HGsoDh0bhVlYIC6ghuwfsohGS7mMQCCRDUs4icuiJwLdtCgqq7OlhqKq2SLvvVetFfw";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>E2E Практики | Тестирование фронтенда</title>
<meta name="description" content="E2E Практики / Тестирование фронтенда: Знакомимся с best practice для тестирования">
<link rel="canonical" href="https://ru.hexlet.io/courses/frontend-testing-browser/lessons/e2e-practice/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="E2E Практики">
<meta property="og:title" content="Тестирование фронтенда">
<meta property="og:description" content="E2E Практики / Тестирование фронтенда: Знакомимся с best practice для тестирования">
<meta property="og:url" content="https://ru.hexlet.io/courses/frontend-testing-browser/lessons/e2e-practice/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="zIxLRlXME80hfeKsO9qPHF2mrnl2qtgXyWZlxCj_2AAjXYBxp7K-rZc-xjQ31X9rna-D036dJrV0hv-Qevg_bg" />
<script src="/vite/assets/inertia-DfXos102.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-BrRXra1y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-cb8xch9l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T20:19:32.754Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"EPO0mBANomPJWScMEecH0yxSHZMgGpIz_sALnGsUwwD_In-v4nMPA38aA5Qd6Pek7FswOSgtbJFDIJHIORMkbg","topics":[{"id":96040,"title":"Привет! А так и задумано, что в третьем уроке упражнение на React Testing Library, которая еще не разбиралась в курсе?","plain_title":"Привет! А так и задумано, что в третьем уроке упражнение на React Testing Library, которая еще не разбиралась в курсе? ","creator":{"public_name":"Show How","id":385327,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":186910,"body":"**Show How**, здравствуйте! Вы правы. Спасибо! Перенес.","topic_id":96040},{"creator":{"public_name":"Show How","id":385327,"is_tutor":false},"id":186803,"body":"UPD: Кажется, оно сбежало из 7го урока)","topic_id":96040}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"E2E Практики","entity_url":null,"active":true}},{"id":68854,"title":"https://prnt.sc/eA0JHoRfSA0U\nприложение не открывается","plain_title":"https://prnt.sc/eA0JHoRfSA0U приложение не открывается ","creator":{"public_name":"Korotkov daniil","id":326113,"is_tutor":false},"comments":[{"creator":{"public_name":"Korotkov daniil","id":326113,"is_tutor":false},"id":144381,"body":"Здравствуйте! Там надо было код исправить и все заработало. Это видимо часть упражнения)","topic_id":68854},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":144344,"body":"**user-f669dee7900b24b6**, здравствуйте! Разбираюсь","topic_id":68854},{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":144385,"body":"О, круто что получилось! А я другой баг обнаружил, думал что в нём проблема)","topic_id":68854}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"E2E Практики","entity_url":null,"active":true}},{"id":75870,"title":"При добавлении import { setupServer } from 'msw/node' по примеру из документации https://mswjs.io/docs/getting-started/integrate/browser падает приложение.\n\n\n> BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.\nThis is no longer the case. Verify if you need this module and configure a polyfill for it.\n\n\nhttps://prnt.sc/Qj3ieG46UIqt","plain_title":"При добавлении import { setupServer } from 'msw/node' по примеру из документации https://mswjs.io/docs/getting-started/integrate/browser падает приложение. BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default. This is no longer the case. Verify if you need this module and configure a polyfill for it. https://prnt.sc/Qj3ieG46UIqt ","creator":{"public_name":"Vladimir","id":26134,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":158211,"body":"**Vladimir**, можете запушить репо своего проекта и приложить ссылку? Обычно мы тут помогаем непосредственно по теории и упржнениям. У нас есть слак комьюнити, где вам могут помочь уже по каким-то специфическим вопросам, как например этот.","topic_id":75870}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"E2E Практики","entity_url":null,"active":true}},{"id":86298,"title":"Здравствуйте, при добавлении таски бэкенд отвечает 404.","plain_title":"Здравствуйте, при добавлении таски бэкенд отвечает 404. ","creator":{"public_name":"Александр Невский","id":602945,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":173570,"body":"**Александр Невский**, здравствуйте! Спасибо за замечание, поправил!","topic_id":86298}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"E2E Практики","entity_url":null,"active":true}},{"id":104575,"title":"Опечатка:\n`constructor(dirver) `","plain_title":"Опечатка: constructor(dirver) ","creator":{"public_name":"Дмитрий Петрук","id":160241,"is_tutor":false},"comments":[{"creator":{"public_name":"Elena Gromova","id":548102,"is_tutor":true},"id":197138,"body":"**Дмитрий Петрук**, поправили, спасибо","topic_id":104575}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"E2E Практики","entity_url":null,"active":true}}],"lesson":{"exercise":null,"units":[{"id":4565,"name":"theory","url":"/courses/frontend-testing-browser/lessons/e2e-practice/theory_unit"}],"links":[],"ordered_units":[{"id":4565,"name":"theory","url":"/courses/frontend-testing-browser/lessons/e2e-practice/theory_unit"}],"id":2031,"slug":"e2e-practice","state":"approved","name":"E2E Практики","course_order":300,"goal":"Знакомимся с best practice для тестирования","self_study":null,"theory_video_provider":"vimeo","theory_video_uid":"558935961","theory":"# E2E Практики\n\n## Hexlet\n\n---\n<!-- https://martinfowler.com/bliki/PageObject.html#footnote-assertions -->\n<!-- https://www.selenium.dev/documentation/en/guidelines_and_recommendations/page_object_models/ -->\n\n\n\n# Что вообще проверяем?\n\n* Соответствие макету\n * поддержка retina-мониторов\n * pixel-perfect\n * контраст / следование style guides\n* Проверка на разных разрешениях экрана\n * десктопной\n * мобильной\n * адаптивных\n* HTML / CSS / JS\n* Шрифты\n\n---\n\n* Работа в разных окружениях\n * кроссбраузерность\n * работа на разных устройствах\n * работа на разных операционных системах\n * корректная работа с разной скоростью интернета\n * корректная работа при включенном расширением AdBlock в браузере\n * анимация / прокрутка / sticky элементы / плавность\n* Контент\n * большой текст\n * орфографии\n * изображения\n\n---\n\n# Что мы делаем\n\n* определяем пользовательские сценарии\n * логин\n * создание аккаунта\n * отправка сообщений\n * покупки\n* автоматизируем сценарии\n* тестируем тесты\n* учитываем кроссбраузерность\n\n---\n\n\n# Даже не пытаемся тестировать\n\n* CAPTCHA\n * Отключаем капчу в тестовом окружении\n * Добавьте хук, позволяющий тестам обходить капчу\n* Двухфакторная аутентификация\n * Отключаем 2FA в тестовом окружении\n * Отключаем 2FA для определенных пользователей в тестовом окружении\n Вы можете использовать учетные данные этого пользователя при автоматизации\n * Отключите 2FA для входа в систему с определенных IP-адресов\n Мы можем установить эти IP-адреса для наших тестовых машин\n---\n# Даже не пытаемся тестировать\n\n* вход на сервисы вроде Gmail и Facebook, с помощью WebDriver это делать не стоит\n * это против условий использования этих сайтов\n * вы рискуете потерять учетную запись\n * это медленно и ненадежно\n * используйте сервис, предоставляющий API для создания тестовых учетных записей\n * работа с API может усложнить работу, но это окупится скоростью, надежностью и стабильностью\n* геттеры / сеттеры\n* нечто нерелевантное / внешнее / постороннее \n\n---\n\n* Поддержка X браузеров на Y платформах - затратно\n* Ведет к ловушке поддержки X×Y реализаций\n* Делайте ваши тесты как можно более [устойчивыми](https://en.wikipedia.org/wiki/Software_brittleness) === тесты не будут сразу ломаться при любом изменении\n\n---\n\n* Ориентируйтесь на перспективу конечного пользователя\n* Мыслите как пользователь\n* Сосредоточьтесь на особенностях приложения, а не на его реализации\n * Чего пытается достичь пользователь?\n * Легко ли найти то, что он(а) ищет?\n * Достигнет ли пользователь своей цели в несколько простых шагов?\n\n---\n\n* Избегайте нагромождений селекторов\n* Убедитесь, что у элемента есть стабильный селектор, который не изменится в следующей версии приложения\n* Выбирайте элементы страницы с умом\n * ID\n * CSS селекторы\n * data-аттрибуты\n * Доступность (aria)\n\n---\n\n* Тесты не должны зависеть друг от друга\n* Не игнорируйте неустойчивые тесты, которые возвращают разные результаты без каких-либо изменений в коде\n* Прогоняйте тесты еще раз, прежде чем заводить issue\n* Убедитесь, что у вас есть подходящие тестовые данные\n\n---\n\n* Напишите отчет\n* Проведите [дымовое тестирование](https://ru.wikipedia.org/wiki/Smoke_test)\n* Разработайте наборы тестов на согласованность\n* Постройте хорошую организационную структуру\n* Нашли баг? Напишите тест, а затем исправьте его\n* Ждите, не спите\n\n\n---\n\n* Тест должен быть простым\n* Используйте CI / уведомления\n* Используйте линтеры, следуйте стилям кодирования и т.д.\n* `eslint-plugin-jest` может предупреждать, когда в тесте нет утверждения (assertion)\n* Вы можете группировать тесты тегам вроде *#smoke*\n* Антипаттерн: Вы читаете отчет => просматриваете код\n\n---\n\n# Подмена бекенда\n\n---\n\n# Mock Service Worker (msw)\n\n* Передовой мок-API\n* Перехватывает запросы на сетевом уровне, а не на уровне приложений\n* Вы можете использовать axios, fetch, xhr, что угодно\n\n---\n\n```javascript\n// handlers.js\nimport { rest } from 'msw'\n\nexport const handlers = [\n rest.post('/login', (req, res, ctx) => {\n // Сохраняем в сессии статус аутентификации пользователя\n sessionStorage.setItem('is-authenticated', 'true')\n return res(\n // Отвечаем кодом 200\n ctx.status(200),\n )\n }),\n // ...\n]\n```\n\n---\n\n```javascript\n// browser.js\nimport { setupWorker } from 'msw'\nimport { handlers } from './handlers.js'\n// Создаем Service Worker с переданными обработчиками запросов\nexport const worker = setupWorker(...handlers)\n```\n\n---\n\n```jsx\n// src/index.js\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport App from './App.jsx'\n\nif (process.env.NODE_ENV === 'development') {\n const { worker } = require('./browser.js')\n worker.start()\n}\n\nReactDOM.render(<App />, document.getElementById('root'))\n```\n\n---\n\n* Вы можете использовать его для разработки и отладки\n* Поддержка REST API и GraphQL\n* Выполнение на стороне клиента\n* Поддержка TypeScript\n* Независимый от фреймворков\n\n---\n\n```javascript\nrest.post('/login', (req, res, ctx) => {\n if (req.body.username === 'real-user') {\n // возвращаем ответ\n // только когда `username` имеет нужное значение\n return\n }\n\n const { authToken } = req.cookies\n if (isValidToken(authToken)) {\n return res(\n ctx.json({ id: 'abc-123', firstName: 'John' }),\n )\n }\n return res(\n ctx.status(403),\n ctx.json({ message: 'Failed to authenticate!' }),\n )\n})\n```\n\n---\n\n# Исправление ответов сервера\n\n```javascript\nrest.get('https://api.github.com/users/:username', async (req, res, ctx) => {\n // Исходный запрос к URL, получаем ответ\n const originalResponse = await ctx.fetch(req)\n const originalResponseData = await originalResponse.json()\n return res(\n ctx.json({\n location: originalResponseData.location,\n firstName: 'Not the real first name',\n }),\n )\n})\n```\n\n---\n\n```javascript\nconst handler = rest.get('/books', (req, res, ctx) => {\n return res(ctx.json({ title: 'The Lord of the Rings' }))\n})\nconst worker = setupWorker(handler)\n\nworker.start({\n onUnhandledRequest(req) {\n console.error(\n 'Found an unhandled %s request to %s',\n req.method,\n req.url.href,\n )\n },\n})\n```\n\n---\n# Page Object Pattern\n\n---\n\n* много тестов\n* много кода в тестах\n* сложно понимать структуру и флоу\n\n\n---\n\n# Page objects\n\n* упрощение разработки\n* упрощение поддержки\n* Высокоуровневый API\n* `DRY`-принцип: создаем переиспользуемый код избегая повторов\n\n---\n\n * page object — обертка над `HTML` страницей или ее частью\n * Ее API специфично для конкретного приложения\n * Манипулируйте элементами страницы, не углубляясь в `HTML`\n\n * `findElementWithClass('album')` => `selectAlbumWithTitle()`\n\n * `findElementWithClass('rating').setText(5)` => `updateRating(5)`\n\n---\n\n\n```javascript\nclass SearchPage {\n constructor(page) {\n this.page = page\n }\n\n async navigate() {\n await this.page.goto('https://mail.ru')\n }\n\n async search(text) {\n await this.page.fill('[data-testid=\"search-input\"]', text)\n await this.page.press('[data-testid=\"search-button\"]', 'Enter')\n }\n}\n```\n\n---\n\n```javascript\nimport SearchPage from './models/search.js'\n\ntest('search', () => {\n const page = await browser.newPage()\n const searchPage = new SearchPage(page)\n\n await searchPage.navigate()\n await searchPage.search('search query')\n\n // ...\n})\n```\n\n---\n# Преимущества\n\n* Если UI страницы изменится\n * тесты изменять не надо\n * нужно изменить код в page object\n* Все изменения касающиеся поддержки этого нового UI находятся в одном месте\n* Все доступные операции или сервисы на странице хранятся в одном месте вместо того, чтобы дублироваться во всех тестах\n* Код тестов легче понять\n* Существует четкое разделение между кодом тестов и кодом, относящимся к html-странице, вроде селекторов и верстки\n\n---\n# Selenium\n\n```javascript\n\ntest(\"login\", () => {\n // заполняем данные на странице входа\n driver.findElement(By.name(\"username\")).sendKeys(\"testUser\");\n driver.findElement(By.name(\"password\")).sendKeys(\"testPassword\");\n driver.findElement(By.name(\"sign-in\")).click();\n\n // проверяем, что появляется тег h1 с текстом \"Hello testUser\" после входа\n driver.findElement(By.tagName(\"h1\")).isDisplayed();\n expect(driver.findElement(By.tagName(\"h1\")).getText()).toBe(\"Hello testUser\");\n}\n\n```\n\n---\n\n```typescript\n\nclass SignInPage {\n protected WebDriver driver;\n\n // <input name=\"user_name\" type=\"text\" value=\"\">\n private usernameBy: By = By.name(\"username\");\n // <input name=\"password\" type=\"password\" value=\"\">\n private passwordBy: By = By.name(\"password\");\n // <input name=\"sign_in\" type=\"submit\" value=\"SignIn\">\n private signinBy: By = By.name(\"sign-in\");\n\n constructor(driver) {\n this.driver = driver;\n }\n\n loginValidUser(userName: string, password: string): HomePage {\n driver.findElement(usernameBy).sendKeys(userName);\n driver.findElement(passwordBy).sendKeys(password);\n driver.findElement(signinBy).click();\n return new HomePage(driver);\n }\n}\n\n```\n\n---\n\n```typescript\n\npublic class HomePage {\n protected driver: WebDriver;\n\n // <h1>Hello userName</h1>\n private By messageBy = By.tagName(\"h1\");\n\n constructor(driver: WebDriver) {\n this.driver = driver;\n if (!driver.getTitle().equals(\"Home Page of logged in user\")) {\n throw new Error(`This is not Home Page of logged in user, current page is: ${driver.getCurrentUrl()}`);\n }\n }\n\n\n getMessageText(): string {\n return driver.findElement(messageBy).getText();\n }\n}\n\n```\n\n---\n\n```javascript\n\ntest(\"login\", () => {\n const signInPage: SignInPage = new SignInPage(driver);\n const homePage: HomePage = signInPage.loginValidUser(\"userName\", \"password\");\n expect(homePage.getMessageText()).toBe(\"Hello userName\"));\n});\n\n```\n\n---\n\n# Правила\n\n* Сами page object никогда не должны ничего тестировать \n* Page object содержит представление страницы\n* Никакой код, связанный с тем, что тестируется, не должен находиться внутри объекта страницы\n* Исключение: убедитесь, что страница отображена верно\n\n---\n\n* Доступные методы представляют операции, возможные на страницы\n* Try not to expose the internals of the page\n\n* Не создавайте объект для всей страницы, только значимые элементы\n * верхний и нижний колонтитулы, список пользователей, логин и т.Д\n* Общедоступные методы представляют услуги, предлагаемые на странице\n* Старайтесь не показывать внутренности страницы\n\n---\n\n Если поведение должно отличаться\n\n```javascript\nclass LoginPage {\n public HomePage loginAs(username: string, password: string) {\n // ... здесь логинимся\n }\n\n public LoginPage loginAsExpectingError(username: string, password: string) {\n // ... здесь неудавшийся логин\n }\n\n public getErrorMessage(): string {\n // здесь проверяем, верное ли сообщение об ошибке выбрасывается\n }\n}\n```\n\n---\n\nМожно наследоваться \n\n```javascript\nclass LoginPage extends Page {\n get username() {\n return $('#username')\n }\n\n get password() {\n return $('#password')\n }\n\n get submitBtn() {\n return $('form button[type=\"submit\"]')\n }\n\n get flash() {\n return $('#flash')\n }\n\n get headerLinks() {\n return $$('#header a')\n }\n\n open() {\n super.open('login')\n }\n\n submit() {\n this.submitBtn.click()\n }\n}\n```\n\n---\n\n# Минусы e2e\n\n\n* медленные\n* нестабильные\n* непредсказуемое поведение\n* изменение UI ломает тест\n* нельзя посмотреть строчку где тест упал\n\n<!-- https://bespoyasov.ru/blog/coin-e2e-with-cypress/ -->\n<!--\n---\n\n# Домашнее задание\n\n\nПротестируйте одностраничное приложение `Simple Todo List`\n\nДля подмены ответов бекенда используйте `Mock Service Worker`\n\n```\n\nhexlet program download frontend-testing-react e2e-best-practice\n\n```\n-->\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":4569,"name":"theory","url":"/courses/frontend-testing-browser/lessons/web-drivers/theory_unit"}],"links":[],"ordered_units":[{"id":4569,"name":"theory","url":"/courses/frontend-testing-browser/lessons/web-drivers/theory_unit"}],"id":2035,"slug":"web-drivers","state":"approved","name":"WebDrivers","course_order":100,"goal":"Знакомимся с инструментами для взаимодействия с браузерами","self_study":null,"theory_video_provider":"vimeo","theory_video_uid":"558045248","theory":"## Web Drivers\n\nWeb Drivers - это инструменты для взаимодействия с браузером.\n\n* Интерфейс удаленного управления, который позволяет анализировать и управлять браузером\n* Платформонезависимый и не зависит от языка\n* Предоставляет набор интерфейсов для нахождения и управления элементами DOM\n* Не имеет прямого отношения к тестированию\n\n## Selenium\n\nSelenium — один из популярных фреймворков для тестирования. Поддерживается всеми основными платформами и на всех браузерах. Он позволяет автоматизировать тестирование, имитировать действия пользователей.\n\nИспользование:\n\n```javascript\nimport { Builder, By, Key, until } from 'selenium-webdriver'\n\ndescribe('web driver', () => {\n let driver\n\n test('google first result', async () => {\n // Создаём инстанс вебдрайвера\n driver = new Builder().forBrowser('chrome').build()\n\n // Выполняем переход на страницу\n driver.get('https://www.google.com')\n\n // Получаем элемент ввода\n const input = driver.findElement(By.name('q'))\n\n // Вызываем на элементе события нажатия клавиш (ввод в поиск и Enter)\n await input.sendKeys('hello', Key.ENTER)\n\n // Дожидаемся пока не появится элемент и получаем его\n const firstResult = await driver.wait(until.elementLocated(By.css('h3')), 10000)\n\n // Выводим содержимое элемента\n console.log(await firstResult.getAttribute('textContent'))\n }, 10000)\n\n afterEach(() => {\n driver.quit()\n })\n})\n```\n\nВ асинхронных запросах промисы должны возвращаться из тестов, иначе тесты не дожидаются выполнение асинхронных операций. Либо нужно использовать `async await`\n\n```javascript\nimport { Builder, By, Key } from 'selenium-webdriver'\n\nconst URL = 'http://svelte3-todo.surge.sh/'\n\ndescribe('web driver', () => {\n let driver\n\n // Создаём драйвер перед выполнением тестов\n beforeAll(() => {\n driver = new Builder()\n .forBrowser('chrome')\n .build()\n })\n\n // Тест добавления таска\n test('add a task', async () => {\n // Выполняем переход на страницу\n driver.get(URL)\n\n // Возвращаем промис из теста\n return driver.findElement(By.className('js-todo-input')).sendKeys('Build App', Key.ENTER)\n // Асинхнронные запросы оборачиваем в цепочку промиса\n .then(() => driver.getPageSource())\n .then((source) => {\n expect(source.includes('Build App')).toBe(true)\n })\n }, 1000)\n\n // Тест отметки таска как пройденного\n test('mark a task complete', () => {\n driver.get(URL)\n\n return driver.findElement(By.className('js-todo-input')).sendKeys('Build App', Key.ENTER) // Создаём таск\n // Перед изменением таска, проверяем что он не завершен, для этого проверяем класс\n .then(() => driver.findElement(By.className('todo-item')).getAttribute('class')) // получаем класс, асинхронный запрос\n .then((className) => {\n // проверяем что класс не содержит 'done'\n expect(className.includes('done')).toBe(false)\n // Вызываем клик по задаче (отмечаем что она завершена)\n // это асинхронная операция, поэтому возвращаем промис\n return driver.findElement(By.css('label')).click()\n })\n // Снова получаем класс, асинхронный запрос, поэтому оборачиваем в цепочку then\n .then(() => driver.findElement(By.className('todo-item')).getAttribute('class'))\n .then((className) => {\n // Проверяем имя класса\n expect(className.includes('done')).toBe(true)\n })\n }, 1000)\n\n test('delete a task', () => {\n driver.get(URL)\n\n // Перед удалением также создаем таск\n return driver.findElement(By.className('js-todo-input')).sendKeys('Build App', Key.ENTER)\n // Кликаем по кнопке удаления\n .then(() => driver.findElement(By.className('delete-todo')).click())\n // Получаем содержимое страницы\n .then(() => driver.getPageSource())\n .then((source) => {\n // Проверяем что таск удалён\n expect(source.includes('Build App')).toBe(false)\n })\n }, 1000)\n\n afterAll(() => {\n driver.quit()\n })\n})\n```\n\nТоже самое с `async await`:\n\n```javascript\nimport { Builder, By, Key } from 'selenium-webdriver'\n\nconst URL = 'http://svelte3-todo.surge.sh/'\n\ndescribe('web driver', () => {\n let driver\n\n // Создаём драйвер перед выполнением тестов\n beforeAll(() => {\n driver = new Builder()\n .forBrowser('chrome')\n .build()\n })\n\n // Тест добавления таска\n test('add a task', async () => {\n // Выполняем переход на страницу\n driver.get(URL)\n\n // Создаём таск\n await driver.findElement(By.className('js-todo-input')).sendKeys('Build App', Key.ENTER)\n const source = await driver.getPageSource()\n expect(source.includes('Build App')).toBe(true)\n }, 1000)\n\n // Тест отметки таска как пройденного\n test('mark a task complete', async () => {\n driver.get(URL)\n\n // Создаём таск\n await driver.findElement(By.className('js-todo-input')).sendKeys('Build App', Key.ENTER)\n // Перед изменением таска, проверяем что он не завершен, для этого проверяем класс\n const classNameBefore = await driver.findElement(By.className('todo-item')).getAttribute('class') // получаем класс\n\n // проверяем что класс не содержит 'done'\n expect(classNameBefore.includes('done')).toBe(false)\n // Вызываем клик по задаче (отмечаем что она завершена)\n await driver.findElement(By.css('label')).click()\n // Снова получаем класс\n const classNameAfter = await driver.findElement(By.className('todo-item')).getAttribute('class')\n\n // Проверяем имя класса\n expect(classNameAfter.includes('done')).toBe(true)\n }, 1000)\n\n test('delete a task', async () => {\n driver.get(URL)\n\n // Перед удалением также создаем таск\n await driver.findElement(By.className('js-todo-input')).sendKeys('Build App', Key.ENTER)\n // Кликаем по кнопке удаления\n await driver.findElement(By.className('delete-todo')).click()\n // Получаем содержимое страницы\n const source = await driver.getPageSource()\n // Проверяем что таск удалён\n expect(source.includes('Build App')).toBe(false)\n }, 1000)\n\n afterAll(() => {\n driver.quit()\n })\n})\n```\n\n## Cypress\n\nCypress — это e2e фреймворк для тестирования на JS, имеет свой тест-раннер, поддерживает множество языков.\n\nКомпонентное тестирование:\n\n```jsx\nimport * as React from 'react'\nimport { mount } from '@cypress/react'\nimport Button from './Button.jsx'\n\nit('Button', () => {\n // Рендерим кнопку\n mount(<Button>Text button</Button>)\n // Кликаем по кнопке\n cy.get('button').contains('Test button').click()\n})\n```\n\n```javascript\n// Тестируем завершения таска\nit('complete todo', () => {\n // Отрываем страницу\n cy.visit('/')\n // Находим элемент ввода, имитируем ввод имени таска и нажатие Enter\n cy.get('.new-todo').type('write tests{enter}')\n // Отмечаем выполнение таска\n cy.contains('.todo-list li', 'write tests').find('.toggle').check()\n\n // Проверяем наличие класса\n cy.contains('.todo-list li', 'write tests').should('have.class', 'completed')\n\n // При установленном плагине cypress-plugin-snapshots можно создавать скриншоты\n cy.get('.todoapp').toMatchImageSnapshot({\n imageConfig: {\n threshold: 0.001,\n },\n })\n})\n\n// Тест добавления таска\nit('adds todos', () => {\n // Открываем страницу\n cy.visit('/')\n\n // Создаём два таска\n cy.get('.new-todo')\n .type('write E2E tests{enter}')\n .type('add API tests as needed{enter}')\n\n // Проверяем что запросы были отправлены\n cy.request('/todos')\n .its('body')\n .should('have.length', 2)\n .and((items) => {\n // ...\n })\n})\n```\n\n## Playwright\n\nPlaywright — библиотека от Microsoft, так же поддерживает множество языков. Не имеет своего тестраннера.\n\nПример использования:\n\n```javascript\n// Подключаем библиотеку\nimport playwright from 'playwright';\n\n(async () => {\n // Тестируем на разных браузеров в цикле\n for (const browserType of ['chromium', 'firefox', 'webkit']) {\n // Запускаем браузер, получаем инстанс браузера\n const browser = await playwright[browserType].launch()\n // Получаем контекст браузера\n const context = await browser.newContext()\n // Получаем инстанс страницы\n const page = await context.newPage()\n // Открываем страницу\n await page.goto('https://mail.ru')\n // Вызываем событие\n await page.click('[data-testid=\"enter-password\"]')\n // Создаем скриншот\n await page.screenshot({ path: `mail-${browserType}.png` })\n // Закрываем браузер\n await browser.close()\n }\n})()\n```\n\nИмитация другого устройства:\n\n```javascript\nimport { webkit, devices } from 'playwright'\n\nconst iPhone11 = devices['iPhone 11 Pro']\n\ndescribe(() => {\n test('Main test', async () => {\n // Запускаем браузер, получаем инстанс браузера\n const browser = await webkit.launch()\n // Получаем контекст устройства, задаём свои настройки\n const context = await browser.newContext({\n ...iPhone11,\n locale: 'en-US',\n geolocation: { longitude: 12.492507, latitude: 41.889938 },\n permissions: ['geolocation'],\n })\n // Получаем инстанс страницы\n const page = await context.newPage()\n // Открываем страницу\n await page.goto('https://maps.google.com')\n // Вызываем событие\n await page.click('text=\"Your location\"')\n // Ждём выполнение запроса\n await page.waitForRequest(/.*preview\\/pwa/)\n // Создаём скриншот\n await page.screeshot({ path: 'iphone-11.png' })\n // Закрываем браузер\n await browser.close()\n })\n})\n```\n\n## Puppeteer\n\nPuppeteer — библиотека с упором на chrome. Синтаксис очень похож на playwright:\n\n```javascript\n// Подключаем библиотеку\nimport puppeteer from 'puppeteer'\n\ndescribe(() => {\n test('Main test', async () => {\n // Запускаем браузер и получаем инстанс браузера\n const browser = await puppeteer.launch()\n // Получаем инстанс страницы\n const page = await browser.newPage()\n // Открываем страницу\n await page.goto('https://mail.ru')\n // Вызываем событие\n await page.click('[data-testid=\"enter-password\"]')\n // Создаем скриншот\n await page.screenshot({ path: 'example.png' })\n\n // Закрываем браузер\n await browser.close()\n })\n})\n```\n"},"id":236,"slug":"frontend-testing-browser","challenges_count":0,"name":"Тестирование фронтенда","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"На этом курсе вы изучите тестирование фронтенда. Вы узнаете больше о написании e2e-тестов с использованием веб-драйверов, работе с ошибками и фантомными падениями. В итоге вы научитесь создавать надежные тесты в браузерной среде с помощью паттерна Page Object для уменьшения хрупкости и дублирования. Вы также научитесь изолировать бэкенд и тестировать фронтенд с помощью быстрого testing-library в связке с Jest и JSDOM. Знания из курса помогают программистам избежать ошибок и повысить надежность своих приложений.","kind":"sandbox","updated_at":"2026-01-20T11:44:42.610Z","language":"javascript","duration_cache":7200,"skills":["Писать надежные тесты в браузерной среде","Писать e2e тесты, используя веб-драйверы","Использовать паттерн Page Object","Тестировать фронтенд с помощью testing-library","Работать с асинхронностью"],"keywords":["e2e","page object","JSDOM","testing library","асинхронность"],"lessons_count":8,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NzEwMywicHVyIjoiYmxvYl9pZCJ9fQ==--8b010297e9086b1093b75ac652d449f074defcb9/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--6067466c2912ca31a17eddee04b8cf2a38c6ad17/image.png"},"recommendedLandings":[],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/frontend-testing-browser/lessons/e2e-practice/theory_unit","version":"8f286f6358a90a7bef2263b3a6edf5a90a94fa42","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Тестирование фронтенда</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: E2E Практики</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"E2E Практики","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"Тестирование фронтенда"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><h1 id="heading-1-1">E2E Практики</h1>
<h2 id="heading-2-2">Hexlet</h2>
<hr/>
<!-- -->
<!-- -->
<h1 id="heading-1-3">Что вообще проверяем?</h1>
<ul>
<li>Соответствие макету
<ul>
<li>поддержка retina-мониторов</li>
<li>pixel-perfect</li>
<li>контраст / следование style guides</li>
</ul>
</li>
<li>Проверка на разных разрешениях экрана
<ul>
<li>десктопной</li>
<li>мобильной</li>
<li>адаптивных</li>
</ul>
</li>
<li>HTML / CSS / JS</li>
<li>Шрифты</li>
</ul>
<hr/>
<ul>
<li>Работа в разных окружениях
<ul>
<li>кроссбраузерность</li>
<li>работа на разных устройствах</li>
<li>работа на разных операционных системах</li>
<li>корректная работа с разной скоростью интернета</li>
<li>корректная работа при включенном расширением AdBlock в браузере</li>
<li>анимация / прокрутка / sticky элементы / плавность</li>
</ul>
</li>
<li>Контент
<ul>
<li>большой текст</li>
<li>орфографии</li>
<li>изображения</li>
</ul>
</li>
</ul>
<hr/>
<h1 id="heading-1-4">Что мы делаем</h1>
<ul>
<li>определяем пользовательские сценарии
<ul>
<li>логин</li>
<li>создание аккаунта</li>
<li>отправка сообщений</li>
<li>покупки</li>
</ul>
</li>
<li>автоматизируем сценарии</li>
<li>тестируем тесты</li>
<li>учитываем кроссбраузерность</li>
</ul>
<hr/>
<h1 id="heading-1-5">Даже не пытаемся тестировать</h1>
<ul>
<li>CAPTCHA
<ul>
<li>Отключаем капчу в тестовом окружении</li>
<li>Добавьте хук, позволяющий тестам обходить капчу</li>
</ul>
</li>
<li>Двухфакторная аутентификация
<ul>
<li>Отключаем 2FA в тестовом окружении</li>
<li>Отключаем 2FA для определенных пользователей в тестовом окружении
Вы можете использовать учетные данные этого пользователя при автоматизации</li>
<li>Отключите 2FA для входа в систему с определенных IP-адресов
Мы можем установить эти IP-адреса для наших тестовых машин</li>
</ul>
</li>
</ul>
<hr/>
<h1 id="heading-1-6">Даже не пытаемся тестировать</h1>
<ul>
<li>вход на сервисы вроде Gmail и Facebook, с помощью WebDriver это делать не стоит
<ul>
<li>это против условий использования этих сайтов</li>
<li>вы рискуете потерять учетную запись</li>
<li>это медленно и ненадежно</li>
<li>используйте сервис, предоставляющий API для создания тестовых учетных записей</li>
<li>работа с API может усложнить работу, но это окупится скоростью, надежностью и стабильностью</li>
</ul>
</li>
<li>геттеры / сеттеры</li>
<li>нечто нерелевантное / внешнее / постороннее</li>
</ul>
<hr/>
<ul>
<li>Поддержка X браузеров на Y платформах - затратно</li>
<li>Ведет к ловушке поддержки X×Y реализаций</li>
<li>Делайте ваши тесты как можно более <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://en.wikipedia.org/wiki/Software_brittleness" rel="noopener noreferrer" target="_blank">устойчивыми</a> === тесты не будут сразу ломаться при любом изменении</li>
</ul>
<hr/>
<ul>
<li>Ориентируйтесь на перспективу конечного пользователя</li>
<li>Мыслите как пользователь</li>
<li>Сосредоточьтесь на особенностях приложения, а не на его реализации
<ul>
<li>Чего пытается достичь пользователь?</li>
<li>Легко ли найти то, что он(а) ищет?</li>
<li>Достигнет ли пользователь своей цели в несколько простых шагов?</li>
</ul>
</li>
</ul>
<hr/>
<ul>
<li>Избегайте нагромождений селекторов</li>
<li>Убедитесь, что у элемента есть стабильный селектор, который не изменится в следующей версии приложения</li>
<li>Выбирайте элементы страницы с умом
<ul>
<li>ID</li>
<li>CSS селекторы</li>
<li>data-аттрибуты</li>
<li>Доступность (aria)</li>
</ul>
</li>
</ul>
<hr/>
<ul>
<li>Тесты не должны зависеть друг от друга</li>
<li>Не игнорируйте неустойчивые тесты, которые возвращают разные результаты без каких-либо изменений в коде</li>
<li>Прогоняйте тесты еще раз, прежде чем заводить issue</li>
<li>Убедитесь, что у вас есть подходящие тестовые данные</li>
</ul>
<hr/>
<ul>
<li>Напишите отчет</li>
<li>Проведите <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://ru.wikipedia.org/wiki/Smoke_test" rel="noopener noreferrer" target="_blank">дымовое тестирование</a></li>
<li>Разработайте наборы тестов на согласованность</li>
<li>Постройте хорошую организационную структуру</li>
<li>Нашли баг? Напишите тест, а затем исправьте его</li>
<li>Ждите, не спите</li>
</ul>
<hr/>
<ul>
<li>Тест должен быть простым</li>
<li>Используйте CI / уведомления</li>
<li>Используйте линтеры, следуйте стилям кодирования и т.д.</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">eslint-plugin-jest</code> может предупреждать, когда в тесте нет утверждения (assertion)</li>
<li>Вы можете группировать тесты тегам вроде <em>#smoke</em></li>
<li>Антипаттерн: Вы читаете отчет => просматриваете код</li>
</ul>
<hr/>
<h1 id="heading-1-7">Подмена бекенда</h1>
<hr/>
<h1 id="heading-1-8">Mock Service Worker (msw)</h1>
<ul>
<li>Передовой мок-API</li>
<li>Перехватывает запросы на сетевом уровне, а не на уровне приложений</li>
<li>Вы можете использовать axios, fetch, xhr, что угодно</li>
</ul>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">// handlers.js
import { rest } from 'msw'
export const handlers = [
rest.post('/login', (req, res, ctx) => {
// Сохраняем в сессии статус аутентификации пользователя
sessionStorage.setItem('is-authenticated', 'true')
return res(
// Отвечаем кодом 200
ctx.status(200),
)
}),
// ...
]</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">// browser.js
import { setupWorker } from 'msw'
import { handlers } from './handlers.js'
// Создаем Service Worker с переданными обработчиками запросов
export const worker = setupWorker(...handlers)</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'
if (process.env.NODE_ENV === 'development') {
const { worker } = require('./browser.js')
worker.start()
}
ReactDOM.render(<App />, document.getElementById('root'))</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<ul>
<li>Вы можете использовать его для разработки и отладки</li>
<li>Поддержка REST API и GraphQL</li>
<li>Выполнение на стороне клиента</li>
<li>Поддержка TypeScript</li>
<li>Независимый от фреймворков</li>
</ul>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">rest.post('/login', (req, res, ctx) => {
if (req.body.username === 'real-user') {
// возвращаем ответ
// только когда `username` имеет нужное значение
return
}
const { authToken } = req.cookies
if (isValidToken(authToken)) {
return res(
ctx.json({ id: 'abc-123', firstName: 'John' }),
)
}
return res(
ctx.status(403),
ctx.json({ message: 'Failed to authenticate!' }),
)
})</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<h1 id="heading-1-9">Исправление ответов сервера</h1>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">rest.get('https://api.github.com/users/:username', async (req, res, ctx) => {
// Исходный запрос к URL, получаем ответ
const originalResponse = await ctx.fetch(req)
const originalResponseData = await originalResponse.json()
return res(
ctx.json({
location: originalResponseData.location,
firstName: 'Not the real first name',
}),
)
})</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">const handler = rest.get('/books', (req, res, ctx) => {
return res(ctx.json({ title: 'The Lord of the Rings' }))
})
const worker = setupWorker(handler)
worker.start({
onUnhandledRequest(req) {
console.error(
'Found an unhandled %s request to %s',
req.method,
req.url.href,
)
},
})</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<h1 id="heading-1-10">Page Object Pattern</h1>
<hr/>
<ul>
<li>много тестов</li>
<li>много кода в тестах</li>
<li>сложно понимать структуру и флоу</li>
</ul>
<hr/>
<h1 id="heading-1-11">Page objects</h1>
<ul>
<li>упрощение разработки</li>
<li>упрощение поддержки</li>
<li>Высокоуровневый API</li>
<li><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">DRY</code>-принцип: создаем переиспользуемый код избегая повторов</li>
</ul>
<hr/>
<ul>
<li>
<p>page object — обертка над <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">HTML</code> страницей или ее частью</p>
</li>
<li>
<p>Ее API специфично для конкретного приложения</p>
</li>
<li>
<p>Манипулируйте элементами страницы, не углубляясь в <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">HTML</code></p>
</li>
<li>
<p><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">findElementWithClass('album')</code> => <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">selectAlbumWithTitle()</code></p>
</li>
<li>
<p><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">findElementWithClass('rating').setText(5)</code> => <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">updateRating(5)</code></p>
</li>
</ul>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">class SearchPage {
constructor(page) {
this.page = page
}
async navigate() {
await this.page.goto('https://mail.ru')
}
async search(text) {
await this.page.fill('[data-testid="search-input"]', text)
await this.page.press('[data-testid="search-button"]', 'Enter')
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">import SearchPage from './models/search.js'
test('search', () => {
const page = await browser.newPage()
const searchPage = new SearchPage(page)
await searchPage.navigate()
await searchPage.search('search query')
// ...
})</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<h1 id="heading-1-12">Преимущества</h1>
<ul>
<li>Если UI страницы изменится
<ul>
<li>тесты изменять не надо</li>
<li>нужно изменить код в page object</li>
</ul>
</li>
<li>Все изменения касающиеся поддержки этого нового UI находятся в одном месте</li>
<li>Все доступные операции или сервисы на странице хранятся в одном месте вместо того, чтобы дублироваться во всех тестах</li>
<li>Код тестов легче понять</li>
<li>Существует четкое разделение между кодом тестов и кодом, относящимся к html-странице, вроде селекторов и верстки</li>
</ul>
<hr/>
<h1 id="heading-1-13">Selenium</h1>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">test("login", () => {
// заполняем данные на странице входа
driver.findElement(By.name("username")).sendKeys("testUser");
driver.findElement(By.name("password")).sendKeys("testPassword");
driver.findElement(By.name("sign-in")).click();
// проверяем, что появляется тег h1 с текстом "Hello testUser" после входа
driver.findElement(By.tagName("h1")).isDisplayed();
expect(driver.findElement(By.tagName("h1")).getText()).toBe("Hello testUser");
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">class SignInPage {
protected WebDriver driver;
// <input name="user_name" type="text" value="">
private usernameBy: By = By.name("username");
// <input name="password" type="password" value="">
private passwordBy: By = By.name("password");
// <input name="sign_in" type="submit" value="SignIn">
private signinBy: By = By.name("sign-in");
constructor(driver) {
this.driver = driver;
}
loginValidUser(userName: string, password: string): HomePage {
driver.findElement(usernameBy).sendKeys(userName);
driver.findElement(passwordBy).sendKeys(password);
driver.findElement(signinBy).click();
return new HomePage(driver);
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">public class HomePage {
protected driver: WebDriver;
// <h1>Hello userName</h1>
private By messageBy = By.tagName("h1");
constructor(driver: WebDriver) {
this.driver = driver;
if (!driver.getTitle().equals("Home Page of logged in user")) {
throw new Error(`This is not Home Page of logged in user, current page is: ${driver.getCurrentUrl()}`);
}
}
getMessageText(): string {
return driver.findElement(messageBy).getText();
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">test("login", () => {
const signInPage: SignInPage = new SignInPage(driver);
const homePage: HomePage = signInPage.loginValidUser("userName", "password");
expect(homePage.getMessageText()).toBe("Hello userName"));
});</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<h1 id="heading-1-14">Правила</h1>
<ul>
<li>Сами page object никогда не должны ничего тестировать</li>
<li>Page object содержит представление страницы</li>
<li>Никакой код, связанный с тем, что тестируется, не должен находиться внутри объекта страницы</li>
<li>Исключение: убедитесь, что страница отображена верно</li>
</ul>
<hr/>
<ul>
<li>
<p>Доступные методы представляют операции, возможные на страницы</p>
</li>
<li>
<p>Try not to expose the internals of the page</p>
</li>
<li>
<p>Не создавайте объект для всей страницы, только значимые элементы</p>
<ul>
<li>верхний и нижний колонтитулы, список пользователей, логин и т.Д</li>
</ul>
</li>
<li>
<p>Общедоступные методы представляют услуги, предлагаемые на странице</p>
</li>
<li>
<p>Старайтесь не показывать внутренности страницы</p>
</li>
</ul>
<hr/>
<p>Если поведение должно отличаться</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">class LoginPage {
public HomePage loginAs(username: string, password: string) {
// ... здесь логинимся
}
public LoginPage loginAsExpectingError(username: string, password: string) {
// ... здесь неудавшийся логин
}
public getErrorMessage(): string {
// здесь проверяем, верное ли сообщение об ошибке выбрасывается
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<p>Можно наследоваться</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">class LoginPage extends Page {
get username() {
return $('#username')
}
get password() {
return $('#password')
}
get submitBtn() {
return $('form button[type="submit"]')
}
get flash() {
return $('#flash')
}
get headerLinks() {
return $$('#header a')
}
open() {
super.open('login')
}
submit() {
this.submitBtn.click()
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<hr/>
<h1 id="heading-1-15">Минусы e2e</h1>
<ul>
<li>медленные</li>
<li>нестабильные</li>
<li>непредсказуемое поведение</li>
<li>изменение UI ломает тест</li>
<li>нельзя посмотреть строчку где тест упал</li>
</ul>
<!-- -->
</div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/frontend-testing-browser/lessons/e2e-practice/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 8</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><button style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root" type="button"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Обсуждения (архив)</span><span class="m_57492dcc mantine-NavLink-description"></span></div></button><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/frontend-testing-browser/lessons/e2e-practice/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto mantine-active m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" type="button"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-Bukl1lYy.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-BrRXra1y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>