---
course: ПиТПМ
lecture: 7
title: "Лекция №7. Автоматизированное тестирование веб-интерфейсов (E2E) и управление асинхронными операциями."
---

# Лекция №7. Автоматизированное тестирование веб-интерфейсов (E2E) и управление асинхронными операциями.

## §7.1. Эволюция E2E-инструментов: от Selenium к Playwright

В первом семестре мы познакомились с **Selenium WebDriver** — классическим инструментом для автоматизации браузера. Мы научились:

- Открывать браузер через new ChromeDriver().
- Находить элементы по ID, CSS, XPath.
- Кликать, вводить текст, читать значения.
- Использовать WebDriverWait для ожидания загрузки элементов.

**Selenium** существует уже более 15 лет. Это проверенный, надежный инструмент, который используется в тысячах проектов по всему миру.

Но у него есть проблемы, и вы с ними **уже столкнулись** (*или еще столкнетесь* 😏).

Болевые точки **Selenium** (или почему индустрия ищет альтернативы) заключаются в проблемах, о которых мы сейчас поговорим.

В 2020 году команда **Microsoft**, которая до этого разрабатывала инструмент **Puppeteer** (*для автоматизации Chrome*), решила создать **Playwright**.

**Их цель была амбициозной**

- сделать инструмент, который решает все перечисленные проблемы и работает одинаково хорошо **со всеми современными браузерами**.

**Ключевая идея Playwright**

- звучит так, что вместо того, чтобы управлять браузером через внешний драйвер, **Playwright** общается с браузером напрямую через **DevTools Protocol** — тот самый протокол, который используют инструменты разработчика `(F12)` в браузере.

Вот как это выглядит в сравнении:

| Характеристика | Selenium | Playwright |
| --- | --- | --- |
| **Способ общения** | Через внешний драйвер (WebDriver) | Напрямую через DevTools Protocol |
| **Количество посредников** | Тест → Драйвер → Браузер | Тест → Браузер |
| **Скорость** | Медленнее (лишний шаг) | Быстрее (прямое общение) |
| **Сложность настройки** | Нужно устанавливать драйвер для каждого браузера | Достаточно установить браузеры через Playwright CLI |
| **Работа с разными браузерами** | Разные реализации WebDriver | Единый API для Chrome, Firefox, Safari |
| **Поддержка современных сайтов** | Хуже (особенно SPA) | Отличная (изначально создавался под SPA) |

Давайте разберем архитектурное отличие **CDP** (*Chrome DevTools Protocol, то, что использует Playwright*) от **WebDriver** поподробнее, потому что это фундаментально меняет то, как работают тесты при работе с тестированием приложений.

Для начала рассмотрим **Selenium WebDriver** (*старый подход*). Представьте, что вы хотите передать сообщение другу, но вы не можете говорить напрямую. Вы пишете письмо, отдаете его курьеру, курьер везет его вашему другу, друг читает, пишет ответ, отдает курьеру, курьер везет обратно.

- Вы — ваш тест.
- Курьер — WebDriver (chromedriver.exe).
- Друг — браузер.

**Каждое действие** (клик, ввод текста) — это отдельное письмо. Курьер может задержаться, потерять письмо или перепутать адресата.

Теперь рассмотрим работу **Playwright** (новый подход). Представьте, что вы можете говорить напрямую по телефону с другом. Вы задаете вопрос, он отвечает мгновенно. Никаких посредников, никаких задержек.

- Вы — ваш тест.
- Телефонная связь — DevTools Protocol (CDP).
- Друг — браузер.

**Playwright** использует тот же протокол, что и **инструменты разработчика** (*DevTools*). Когда вы открываете `F12` в браузере, вы видите элементы, сетевые запросы, консоль — всё это работает через **CDP**. **Playwright** просто использует этот же канал для управления браузером.

Что это дает на практике:

`1.` **Скорость.** Команды выполняются быстрее, потому что нет лишнего посредника.

`2.` **Надежность.** Меньше точек отказа — меньше шансов, что что-то пойдет не так.

`3.` **Больше возможностей.** Playwright может перехватывать сетевые запросы, эмулировать мобильные устройства, подменять геолокацию — всё то, что Selenium делает с трудом или не делает вовсе.

Важный нюанс

Playwright управляет браузером снаружи, но имеет доступ к его внутреннему состоянию. Это позволяет ему:

- Слушать сетевые запросы (перехватывать, подменять, проверять).
- Внедрять JavaScript-код в контекст страницы.
- Эмулировать разные устройства (iPhone, iPad, десктоп) без изменения кода.

**Selenium** тоже умеет выполнять **JavaScript**, но делает это через **IJavaScriptExecutor**, что менее удобно и медленнее.

Простая аналогия:

- **Selenium** похож на управление роботом через пульт: вы нажимаете кнопку, он делает действие, но вы не знаете, что происходит внутри его «мозга».
- **Playwright** — вы подключены к роботу через кабель и видите все его внутренние процессы. Вы можете не только давать команды, но и читать его мысли (состояние DOM, сетевые запросы, консоль).

## §7.2. Асинхронность как фундамент: async/await в тестах

Когда в первом семестре мы писали тесты на **Selenium**, они выглядели следующим образом:

csharp

```csharp
[Fact]
public void Login_ValidUser_ShowsDashboard()
{
    var driver = new ChromeDriver();
    driver.Navigate().GoToUrl("https://mysite.com/login");
    driver.FindElement(By.Id("username")).SendKeys("admin");
    driver.FindElement(By.Id("password")).SendKeys("12345");
    driver.FindElement(By.Id("login-btn")).Click();
    
    var welcome = driver.FindElement(By.CssSelector(".welcome"));
    Assert.True(welcome.Displayed);
    
    driver.Quit();
}
```

Это **синхронный код**. Каждая команда выполняется последовательно:

`1.` Открыли страницу — ждем, пока загрузится.

`2.` Нашли поле логина — нашли, идем дальше.

`3.` Ввели текст — ввели, идем дальше.

`4.` Нажали кнопку — нажали, идем дальше.

**Код** выполняется **по шагам**, один за другим. Пока одна команда не завершится, следующая не начнется.

Это просто и понятно. Мы все привыкли к такому стилю. Но у этого подхода **есть проблема**.

Когда ваш **тест** отправляет команду **в браузер** (например, *«найти элемент»* или *«перейти по ссылке»*), происходит вот что:

`1.` Ваш код отправляет команду.

`2.` Команда проходит через сеть/процесс к браузеру.

`3.` Браузер выполняет команду.

`4.` Результат возвращается обратно.

`5.` Ваш код получает результат и продолжает работу.

Это операция **ввода-вывода** (*I/O*). Такие операции, в отличие от вычислений (например, сложение чисел), занимают **относительно много времени**.

В чем же проблема **синхронного подхода**? Когда вы выполняете **синхронную операцию**, **поток выполнения** (*thread*) вашей программы блокируется. Он просто стоит и ждет, пока придет ответ от браузера. В это время он ничего не делает — просто тратит ресурсы впустую.

![](/images/lectures/pitpm/07/image-01.webp)

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

**Playwright** с самого начала спроектирован как **асинхронный**. Это означает, что когда он отправляет команду в браузер, он не ждет в блокирующем режиме. Он говорит: *«Я отправил команду, а когда придет ответ — я тогда продолжу»*. В это время поток может делать другие полезные вещи (в контексте тестов — *готовить следующий запрос, обрабатывать другой* тест и т.д.).

![](/images/lectures/pitpm/07/image-02.webp)

Давайте посмотрим на один и тот же тест, написанный в двух подходах.

Что изменилось:

`1.` Ключевое слово **async** в сигнатуре метода.

`2.` Возвращаемый тип **Task** (или **Task<T>** для методов с возвратом).

`3.` Ключевое слово await перед каждым асинхронным вызовом.

`4.` Все методы Playwright возвращают Task и требуют await.

На первый взгляд — просто добавили **await** перед каждой строкой. Но за этим стоят серьезные архитектурные изменения.

Когда вы пишете **await page.GotoAsync("...")**, происходит следующее:

`1.` Метод **GotoAsync** возвращает **Task** — объект, который представляет собой «обещание» выполнить действие в будущем.

`2.` Ключевое слово **await** говорит компилятору: *«Если задача еще не завершена, верни управление вызывающему коду. Когда задача завершится, продолжай выполнение с этой точки»*.

`3.` Поток выполнения не блокируется. Он может обслуживать другие задачи (например, параллельные тесты).

Простая аналогия – представьте, что вы заказываете еду в ресторане.

- **Синхронный подход (Selenium):** Вы стоите у стойки и смотрите, как повар готовит. Вы не можете отойти, пока еда не будет готова. Вы блокируете очередь.
- **Асинхронный подход (Playwright):** Вы делаете заказ, получаете номерок и садитесь за столик. Повар готовит, а вы в это время можете полистать меню, ответить на сообщения или просто отдохнуть. Когда еда готова, вам приносят.

А как асинхронность борется с race conditions? Race condition (состояние гонки) — это ситуация, когда результат программы зависит от того, в каком порядке выполняются операции. В контексте E2E-тестов это часто выглядит так:

`1.` Тест нажал кнопку.

`2.` Сервер начал обрабатывать запрос.

`3.` Тест пытается найти элемент, который появится после обработки запроса.

`4.` Элемент еще не появился → тест падает.

В **Selenium** вы решали эту проблему с помощью **WebDriverWait**:

csharp

```csharp
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
var button = wait.Until(
   ExpectedConditions.ElementToBeClickable(
      By.Id("submit")
   )
);
```

Вы вручную говорили: *«Подожди, пока кнопка станет кликабельной»*. Это работало, но требовало дисциплины. Забыли написать **WebDriverWait** — получили **flaky-тест**.

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

csharp

```csharp
// Playwright автоматически ждет, 
// пока элемент станет готов для действия
// Не нужно писать явный WebDriverWait!
await page.ClickAsync("#submit");
```

Что происходит внутри:

`1.` **Playwright** получает команду *«кликнуть по #submit»*.

`2.` Он асинхронно проверяет: *«Этот элемент видим? Стабилен? Не перекрыт другим элементом?»*.

`3.` Если нет — он ждет (не блокируя поток) и проверяет снова.

`4.` Как только элемент становится готов — выполняет клик.

`5.` Если элемент не появился за таймаут — бросает понятную ошибку.

В **Selenium** вы ждали вручную. В **Playwright** ожидание встроено в каждый метод.

Что это дает на практике? Меньше кода для стабильности:

| Характеристика | Selenium | Playwright |
| --- | --- | --- |
| **Ожидания элементов** | Нужно явно писать WebDriverWait для каждого действия | Ожидания встроены в методы, не нужно писать дополнительный код |
| **Риск flaky-тестов** | Если забыли WebDriverWait — тест может стать flaky | Flaky-тесты встречаются реже, потому что ожидания работают автоматически |
| **Настройка таймаутов** | Сложно настроить правильное время ожидания | Таймаут настраивается глобально для всего теста |

Поскольку **Playwright** не блокирует поток во время ожидания, вы можете запускать несколько тестов параллельно, и они будут выполняться быстрее.

Правило для запоминания

В Playwright каждое действие с браузером — асинхронное. Всегда используйте **await** перед вызовом методов **Playwright**. **Если забыли — тест упадет**

### §7.3. «Умные» ожидания: уход от Thread.Sleep и WebDriverWait

В **лекции №4** первого семестра мы подробно разбирали **антипаттерн «Спящий тест»**:

csharp

```csharp
// ❌ ПЛОХО: гадаем, сколько времени нужно ждать
Thread.Sleep(3000); // Ждем 3 секунды "на всякий случай"
driver.FindElement(By.Id("submit")).Click();
```

И даже правильный вариант с **WebDriverWait** выглядел громоздко:

csharp

```csharp
// ⚠️ ЛУЧШЕ, но все равно приходится писать лишний код
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
var button = wait.Until(
   ExpectedConditions.ElementToBeClickable(
      By.Id("submit")
   )
);
button.Click();
```

Проблема **Selenium** в том, что он не знает, готов ли элемент к взаимодействию. Он просто ищет элемент в **DOM** (*структуре страницы*) и пытается с ним работать. А если элемент есть в **DOM**, но еще не отрисовался или перекрыт другим блоком — **Selenium** падает.

Вы, как **разработчик теста**, вынуждены вручную объяснять **Selenium**: *«Подожди, пока кнопка станет кликабельной»*. И если вы забыли это сделать — тест становится **нестабильным** (*flaky*).

В **Playwright** подход кардинально другой. Перед тем как выполнить любое действие (клик, ввод текста, наведение), **Playwright** автоматически **выполняет серию проверок**. Это называется **Actionability Checks** (проверки готовности к действию).

Что именно проверяет Playwright перед кликом или вводом текста:

| Проверка | Что это значит | Пример проблемы |
| --- | --- | --- |
| **Видим ли элемент?** | Элемент не скрыт CSS-свойством display: none или visibility: hidden | Элемент есть в коде, но еще не появился на экране |
| **Стабилен ли элемент?** | Элемент не двигается по странице (например, не анимируется) | Анимация появления, слайдер, бегущая строка |
| **Не перекрыт ли элемент?** | Никакой другой элемент не находится поверх него | Всплывающая реклама, модальное окно, загрузочный спиннер |
| **Не отключен ли элемент?** | У элемента нет атрибута disabled | Кнопка "Отправить" заблокирована, пока не заполнены все поля |

Что это значит для вас, как для разработчика тестов? Вы просто пишете:

csharp

```csharp
// Всего одна строка. Playwright сам сделает все проверки!
await page.ClickAsync("#submit");
```

**Playwright** не просто кликает. Он:

`1.` Ждет, пока элемент появится в DOM.

`2.` Проверяет, что он видим.

`3.` Проверяет, что он стабилен (не двигается).

`4.` Проверяет, что он не перекрыт.

`5.` Проверяет, что он активен.

`6.` И только после этого выполняет клик.

Если элемент **не готов** в течение таймаута (*по умолчанию 30 секунд*), **Playwright** выбросит понятную ошибку с указанием, какая именно проверка не прошла.

Простая аналогия:

- **Selenium** — это как человек, который пытается пожать руку, даже не посмотрев, есть ли перед ним человек. Он просто машет рукой в пустоту и удивляется, почему никто не ответил.
- **Playwright** — это как человек, который сначала смотрит на собеседника, убеждается, что он смотрит в ответ, протягивает руку, ждет ответного рукопожатия и только потом здоровается.

Вторая гениальная фишка **Playwright** — это **Web-First Assertions** (*утверждения, ориентированные на веб*).

В **Selenium** вы писали так:

csharp

```csharp
var message = driver.FindElement(By.Id("success"));
// Если сообщение еще не появилось — тест упадет
Assert.True(message.Displayed);
```

Вы делали проверку один раз. Если сообщение появлялось через 2 секунды, а вы проверили через 1 секунду — тест падал. Выход — снова использовать **WebDriverWait**:

csharp

```csharp
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
var message = wait.Until(d => d.FindElement(By.Id("success")));
Assert.True(message.Displayed);
```

В **Playwright** проверки работают по-другому. Они автоматически **повторяются** (*retry*), пока условие не выполнится или не истечет таймаут.

csharp

```csharp
// Playwright будет проверять это условие снова и снова
// пока сообщение не появится или не пройдет 5 секунд
await Expect(page.Locator("#success")).ToBeVisibleAsync();
```

Что происходит внутри:

`1.` **Playwright** проверяет, видим ли элемент.

`2.` Если нет — он ждет короткое время (например, 100 мс).

`3.` Проверяет снова.

`4.` И так до тех пор, пока элемент не появится или не истечет таймаут.

Это называется **автоматическим ретраем** (*auto-retry*). Именно это делает тесты на **Playwrigh**t стабильными без лишнего кода.

Давайте посмотрим на один и тот же сценарий (клик по кнопке и проверка появления сообщения) в двух подходах.

Вот полный пример теста на **Playwright** для **.NET**, в котором нет ни одного **Thread.Sleep** или явного **WebDriverWait**:

csharp

```csharp
[Fact]
public async Task UserCanAddProductToCart()
{
    // 1. Настраиваем Playwright
    using var playwright = await Playwright.CreateAsync();
    await using var browser = await playwright.Chromium.LaunchAsync();
    var page = await browser.NewPageAsync();
    
    // 2. Переходим на сайт (ждет загрузки автоматически)
    await page.GotoAsync("https://myshop.com");
    
    // 3. Ищем товар (ждет, пока появится поле поиска)
    await page.FillAsync("#search-input", "iPhone 15");
    await page.ClickAsync("#search-button");
    
    // 4. Ждем, пока появится список товаров (автоматически)
    // Кликаем по кнопке "В корзину" у первого товара
    await page.ClickAsync(".product-card:first-child .add-to-cart");
    
    // 5. Переходим в корзину (ждет, пока кнопка станет кликабельной)
    await page.ClickAsync("#cart-icon");
    
    // 6. Проверяем, что товар в корзине (автоматический retry)
    await Expect(page.Locator(".cart-item"))
                     .ToHaveTextAsync("iPhone 15");
    
    // 7. Оформляем заказ (ждет, пока кнопка станет активной)
    await page.ClickAsync("#checkout-button");
    
    // 8. Проверяем, что заказ оформлен (автоматический retry)
    await Expect(page.Locator(".order-confirmation"))
                     .ToBeVisibleAsync();
}
```

Где здесь ожидания?

- **Нет Thread.Sleep** — мы не гадаем, сколько времени нужно ждать.
- **Нет WebDriverWait** — не нужно писать сложные условия.
- **Нет ручных ожиданий** — Playwright сам решает, когда элемент готов.

Если страница загрузилась за 0.5 секунды — тест продолжается сразу. Если за 5 секунд — **Playwright** подождет 5 секунд. Если за 31 секунду — тест упадет с ошибкой, и вы точно будете знать, что проблема в производительности, а не в тесте.

Почему это работает. Самое основное, это встроенные таймауты. У **Playwright** есть **разумные таймауты** по умолчанию:

- **Таймаут для действий (ClickAsync, FillAsync, GotoAsync)** — 30 секунд.
- **Таймаут для проверок (Expect)** — 5 секунд.

Вы можете изменить их глобально для всех тестов или локально для конкретного действия:

csharp

```csharp
// Увеличиваем таймаут для конкретной проверки
await Expect(page.Locator(".slow-element"))
    .ToBeVisibleAsync(new() { Timeout = 15000 });
```

Но в большинстве случаев стандартные настройки работают отлично.

А что делать, если элемент все равно не находится? Иногда **Playwright** не может найти элемент, потому что он появляется только после выполнения **JavaScript-кода** на странице. В этом случае вы можете использовать дополнительные методы ожидания:

csharp

```csharp
// Ждем, пока элемент появится в DOM
await page.WaitForSelectorAsync("#dynamic-element");

// Или ждем, пока исчезнет загрузочный спиннер
await page.WaitForSelectorAsync(".loader", new() { 
   State = WaitForSelectorState.Hidden 
});
```

Но даже в этом случае вы не пишете **Thread.Sleep** — вы ждете конкретное условие, а не произвольное время.

В заключение, хочется подытожить ключевыми отличиями двух подходов, а именно **Selenium** и **Playwright**.

| Что делаем | Selenium | Playwright |
| --- | --- | --- |
| **Ожидание кликабельности** | Пишем WebDriverWait + ExpectedConditions | Встроено в ClickAsync() |
| **Ожидание появления элемента** | Пишем WebDriverWait + FindElement | Встроено в Expect().ToBeVisibleAsync() |
| **Количество строк кода для ожиданий** | 3-4 строки на каждое действие | 0 строк — всё встроено |
| **Риск забыть ожидание** | Высокий (тест станет flaky) | Низкий (ожидания всегда работают) |

## §7.4. Современный Page Object Model для Playwright: Locators и Page Factory

В лекции №4 первого семестра мы разбирали **Page Object Model** для **Selenium**. Класс страницы выглядел примерно так:

csharp

```csharp
public class LoginPage
{
    private readonly IWebDriver _driver;
    
    // Локаторы через By.Id, By.XPath, By.CssSelector
    private By UserNameField => By.Id("username");
    private By PasswordField => By.Id("password");
    private By LoginButton => By.XPath("//button[text()='Войти']");
    
    public LoginPage(IWebDriver driver)
    {
        _driver = driver;
    }
    
    public HomePage LoginAs(string username, string password)
    {
        _driver.FindElement(UserNameField).SendKeys(username);
        _driver.FindElement(PasswordField).SendKeys(password);
        _driver.FindElement(LoginButton).Click();
        return new HomePage(_driver);
    }
}
```

Проблемы этого подхода:

`1.` **Локаторы хрупкие.** Если дизайнер поменял структуру `HTML` — `XPath` ломается.

`2.` **Сложно читать.** `By.XPath("//div[3]/form/button[1]")` — это магия, а не инструкция.

`3.` **Найдет что угодно.** `XPat`h может найти неправильный элемент, если структура страницы изменилась.

`4.` **Нет приоритетов.** **Selenium** не подсказывает, какой локатор лучше использовать.

В **Playwright** полностью отказались от старого подхода с `By.Id`, `By.XPath` и `FindElement`. Вместо этого используется **Locators API** — новый, более умный способ поиска элементов.

**Locator**

— это объект, который представляет собой инструкцию по поиску элемента на странице. Но в отличие от **Selenium**, **Locator** в **Playwright** — это ленивый объект. Он не ищет элемент сразу, а только запоминает, как его найти.

Реальный поиск происходит только в момент действия (клик, ввод текста).

csharp

```csharp
// Locator — это просто инструкция, элемент еще не найден
var submitButton = page.Locator("#submit");

// Только здесь Playwright реально ищет элемент 
// и проверяет его готовность
await submitButton.ClickAsync();
```

Почему это круто:

`1.` **Автоматические ожидания.** Locator сам ждет, пока элемент появится и станет готов к действию.

`2.` **Переиспользование.** Один Locator можно использовать в разных методах.

`3.` **Читаемость.** Locator строится как цепочка понятных методов.

В **Playwright** есть четкая иерархия того, как искать элементы. Разработчики **Playwright** рекомендуют использовать локаторы в следующем порядке приоритета:

**1. GetByRole (САМЫЙ ПРИОРИТЕТНЫЙ)**

Ищет элемент по его роли **в доступности** (*ARIA*). Это самый надежный способ, потому что роли редко меняются, даже если дизайн перерисовывают.

csharp

```csharp
// Найти кнопку по роли "button" и тексту "Войти"
var loginButton = page.GetByRole(AriaRole.Button, new() { 
   Name = "Войти" 
});

// Найти поле ввода по роли "textbox"
var usernameField = page.GetByRole(AriaRole.Textbox, new() { 
   Name = "Имя пользователя" 
});
```

Когда использовать

Всегда, когда элемент имеет правильную ARIA-роль. Это делает тесты устойчивыми к изменениям вёрстки.

**2. GetByText**

Ищет элемент по тексту, который видит пользователь.

csharp

```csharp
// Найти кнопку с текстом "Добавить в корзину"
var addToCartButton = page.GetByText("Добавить в корзину");

// Найти заголовок с текстом "Добро пожаловать"
var welcomeHeader = page.GetByText("Добро пожаловать");
```

Когда использовать

Для ссылок, кнопок, заголовков — всего, что имеет понятный текстовый контент.

**3. GetByLabel**

Ищет элемент по связанному с ним тексту `label` (для полей ввода).

csharp

```csharp
// Найти поле ввода, рядом с которым есть label "Email"
var emailField = page.GetByLabel("Email");

// Найти поле ввода, рядом с которым есть label "Пароль"
var passwordField = page.GetByLabel("Пароль");
```

Когда использовать

Для полей ввода, которые имеют HTML-тег `<label>` или атрибут `aria-label`.

**4. GetByTestId**

Ищет элемент по специальному атрибуту `data-testid`.

csharp

```csharp
// Найти элемент с атрибутом data-testid="submit-button"
var submitButton = page.GetByTestId("submit-button");
```

Когда использовать

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

**5. GetByPlaceholder**

Ищет поле ввода по `placeholder` (*подсказке внутри поля*).

csharp

```csharp
var searchField = page.GetByPlaceholder("Поиск товаров...");
```

**6. CSS-селекторы (В ПОСЛЕДНЮЮ ОЧЕРЕДЬ)**

Только если ничего из перечисленного выше не подходит.

csharp

```csharp
var element = page.Locator(".product-card .price");

var element2 = page.Locator("#main-content");
```

Теперь давайте сравним **старый** и **новый подход**.

Теперь давайте посмотрим, как выглядит **Page Object Model** с использованием **Locators API** в **Playwright**.

| Характеристика | Selenium (By.Id / XPath) | Playwright (Locators API) |
| --- | --- | --- |
| **Читаемость** | By.XPath("//div[3]/form/button[1]") — магия | GetByRole(AriaRole.Button, "Войти") — понятно |
| **Устойчивость** | Ломается при изменении структуры HTML | Устойчив, если не меняется роль или текст |
| **Приоритеты** | Нет подсказок, какой локатор лучше | Четкая иерархия (Role > Text > Label > TestId) |
| **Автоожидания** | Нужно писать WebDriverWait отдельно | Встроены в Locator |
| **Рекомендации** | Все локаторы считаются равными | Есть четкие рекомендации, что использовать |
| **Поддержка доступности** | Не учитывает ARIA-роли | Создан с учетом доступности (ARIA) |

Важные правила для начинающих специалистов:

`1.` Никогда не используйте `XPath`, если можно использовать `Role`, `Text` или `Label`. `XPath` — это крайний случай.

`2.` Если разработчики добавили `data-testid` — используйте его. Это самый надежный способ.

`3.` Храните все локаторы внутри **Page Object'ов**. Никаких локаторов в тестах!

`4.` Locator живет до момента действия. Он не ищет элемент сразу, а только при клике или вводе текста. Это нормально.

## §7.5. Расширенные возможности Playwright: Фикстуры, Трейсы и Сниппеты

В предыдущих параграфах мы писали тесты, и в каждом из них приходилось повторять один и тот же код:

csharp

```csharp
[Fact]
public async Task SomeTest()
{
    // Один и тот же код в каждом тесте!
    using var playwright = await Playwright.CreateAsync();
    await using var browser = await playwright.Chromium.LaunchAsync();
    var page = await browser.NewPageAsync();
    
    // ... сам тест ...
}
```

**Это дублирование.** Если у вас 20 тестов — этот код повторяется 20 раз. Если нужно изменить настройки браузера (например, запускать в headless-режиме) — придется править все 20 тестов.

**Headless-режим**

— это запуск веб-браузера без графического пользовательского интерфейса (GUI).

Браузер работает в фоновом режиме, как консольное приложение:

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

**Playwright** предлагает **элегантное решение** — **фикстуры** (*Fixtures*).

Что такое **фикстуры**?

**Фикстура (Fixture)**

— это просто «заготовка» или «шаблон», который подготавливает окружение для тестов.

Представьте, что вы каждый день готовите завтрак. Каждый раз вы:

`1.` Достаете сковороду.

`2.` Наливаете масло.

`3.` Разбиваете яйца.

`4.` Жарите.

Это одни и те же действия каждый день. Было бы удобно иметь готовый «набор для завтрака», где сковорода уже на плите, масло налито, а яйца разбиты. Вы просто приходите и начинаете жарить.

**Фикстуры** в Playwright — это такой же «набор для завтрака». Они автоматически:

- Создают экземпляр Playwright.
- Запускают браузер.
- Создают контекст (изолированное окружение для теста).
- Создают страницу.

И всё это — без **дублирования** кода в каждом тесте!

В **Playwright** для **.NET** есть специальный **базовый класс** — **PageTest**. Если ваш тестовый класс наследуется от него, вы получаете готовые объекты **Page**, **Browser** и **Context** без лишнего кода.

Как это выглядит:

csharp

```csharp
using Microsoft.Playwright.NUnit;
using NUnit.Framework;

// Наследуемся от PageTest — получаем всё готовое!
public class LoginTests : PageTest
{
    [Test]
    public async Task Login_ValidCredentials_ShowsDashboard()
    {
        // Page уже создан! 
        // Не нужно писать Playwright.CreateAsync() и т.д.
        await Page.GotoAsync("https://mysite.com/login");
        await Page.FillAsync("#username", "admin");
        await Page.FillAsync("#password", "12345");
        await Page.ClickAsync("#login-btn");
        
        await Expect(Page.Locator(".welcome")).ToBeVisibleAsync();
    }
    
    [Test]
    public async Task Login_InvalidPassword_ShowsError()
    {
        // Тот же Page, но в другом тесте — новый, чистый экземпляр!
        await Page.GotoAsync("https://mysite.com/login");
        await Page.FillAsync("#username", "admin");
        await Page.FillAsync("#password", "wrong");
        await Page.ClickAsync("#login-btn");
        
        await Expect(Page.Locator(".error")).ToBeVisibleAsync();
    }
}
```

Что мы получили:

`1.` Никакого дублирования. Не нужно писать **Playwright.CreateAsync()** в каждом тесте.

`2.` Автоматическая изоляция. Каждый тест получает новую страницу. Тесты не влияют друг на друга.

`3.` Автоматическая очистка. После теста браузер закрывается автоматически.

В **Selenium** мы сами отвечали за то, чтобы тесты не влияли друг на друга. Нужно было создавать новый драйвер для каждого теста, закрывать его после теста, чистить **cookies** и **localStorage**.

В **Playwright** с **PageTes**t это работает автоматически:

| Что делает Selenium | Что делает Playwright с PageTest |
| --- | --- |
| **Вы сами создаете ChromeDriver** | PageTest создает IPage автоматически |
| **Вы сами закрываете драйвер (driver.Quit())** | PageTest закрывает всё автоматически |
| **Нужно чистить cookies вручную** | Каждый тест получает новую страницу — чистую |
| **Тесты могут влиять друг на друга** | Тесты полностью изолированы |

Простая аналогия:

- **Selenium** — как общий стол в офисе. Все едят за одним столом, кто-то может оставить крошки, и следующий человек будет сидеть в беспорядке.
- **Playwright с фикстурами** — как отдельный столик в кафе для каждого посетителя. Когда вы уходите, столик полностью убирают. Следующий посетитель получает чистый стол.

Перейдем к следующем части. Представьте, что =у вас есть много **Page Object'ов** (`LoginPage`, `HomePage`, `CartPage`, `PaymentPage`). Каждый раз создавать их вручную — утомительно:

csharp

```csharp
var loginPage = new LoginPage(Page);
var homePage = new HomePage(Page);
var cartPage = new CartPage(Page);
```

**Page Factory**

— это паттерн, который централизует создание Page Object'ов.

csharp

```csharp
public class PageFactory
{
    private readonly IPage _page;
    
    public PageFactory(IPage page)
    {
        _page = page;
    }
    
    public LoginPage LoginPage 
       => new LoginPage(_page);

    public HomePage HomePage 
       => new HomePage(_page);

    public CartPage CartPage 
       => new CartPage(_page);

    public PaymentPage PaymentPage 
       => new PaymentPage(_page);
}
```

Теперь тест выглядит так:

csharp

```csharp
public class ShopTests : PageTest
{
    [Test]
    public async Task UserCanBuyProduct()
    {
        // Один раз создаем фабрику
        var factory = new PageFactory(Page);
        
        // Используем готовые объекты страниц
        await factory.LoginPage.NavigateToAsync();
        await factory.LoginPage.LoginAsAsync("user", "pass");
        
        await factory.HomePage.SearchForProductAsync("iPhone");

        await factory.CartPage.AddToCartAsync();

        await factory.CartPage.ProceedToCheckoutAsync();
        
        await factory.PaymentPage.PayWithCardAsync(
           "1234", "12/25", "123"
        );
        
        var isConfirmed = await factory.PaymentPage
                                       .IsOrderConfirmedAsync();
        Assert.True(isConfirmed);
    }
}
```

Что дает **Page Factory**:

`1.` **Централизованное создание.** Все страницы создаются в одном месте.

`2.` **Читаемость.** В тесте не видно new LoginPage(Page) — только factory.LoginPage.

`3.` **Легкость поддержки.** Если нужно изменить способ создания страниц — правим только фабрику.

**Самая большая проблема в E2E-тестировании**

— понять, почему упал тест. В **Selenium** вы видели только лог ошибки. Чтобы понять, что произошло, нужно было запускать тест заново с дебаггером или добавлять скриншоты вручную.

Playwright предлагает три мощных инструмента для отладки:

**1. Скриншоты (Screenshots)**

**Playwright** может автоматически делать скриншот в момент падения теста.

csharp

```csharp
// В настройках теста
[Test]
public async Task MyTest()
{
    // Если тест упадет, Playwright сделает скриншот
    // и сохранит его в папке test-results
}
```

Или вы можете сделать скриншот вручную в любом месте теста:

csharp

```csharp
await Page.ScreenshotAsync(new()
{
    Path = "screenshot.png",
    FullPage = true // Скриншот всей страницы
});
```

**2. Видеозапись (Videos)**

**Playwright** может записывать видео всего теста. Вы видите, что происходило в браузере с самого начала до падения.

csharp

```csharp
// В настройках браузера
await using var browser = await playwright.Chromium.LaunchAsync(new()
{
    Headless = false
});

var context = await browser.NewContextAsync(new()
{
    RecordVideoDir = "videos/" // Куда сохранять видео
})
```

После теста у вас будет **MP4-файл** с записью всего, что делал браузер.

**3. Трейсы (Trace Viewer) — самый мощный инструмент**

**Трейс (Trace)**

— это запись всего, что происходило в тесте: каждый клик, каждый ввод текста, каждое изменение **DOM**, сетевые запросы, консольные ошибки. Это как *«черный ящик»* для теста.

csharp

```csharp
// Включаем запись трейса
await context.Tracing.StartAsync(new()
{
    Screenshots = true,
    Snapshots = true,
    Sources = true
});
// ... выполняем тест ...

// Сохраняем трейс
await context.Tracing.StopAsync(new()
{
    Path = "trace.zip"
});
```

После теста у вас получится файл **trace.zip**. Вы открываете его в **Trace Viewer** (встроенный инструмент **Playwright**) и видите:

`1.` Пошаговую запись всех действий теста.

`2.` Скриншоты каждого шага.

`3.` DOM-дерево в каждый момент времени.

`4.` Сетевые запросы (что и когда отправлялось на сервер).

`5.` Консоль (ошибки JavaScript, логи).

`6.` Временную шкалу — что происходило в какой момент.

Простая аналогия:

- **Лог ошибки в Selenium** — это как получить СМС: «Что-то пошло не так». И вы гадаете, что именно.
- **Трейс в Playwright** — это как просмотр видео с камер наблюдения, где видно каждое движение, каждое действие, каждую ошибку.

Сравнение отладки в двух подходах, а именно **Selenium** и **Playwright**, выглядит следующим образом:

| Возможность / Инструмент | Selenium | Playwright |
| --- | --- | --- |
| **Ошибка** | StackTrace + сообщение | StackTrace + сообщение |
| **Скриншот** | Нужно добавлять вручную | Делается автоматически при падении |
| **Видео** | Нет встроенной поддержки | Есть встроенная запись видео |
| **Что произошло до ошибки** | Неизвестно | Трейс показывает все шаги |
| **Сетевые запросы** | Не видно | Видно в трейсе |
| **Состояние DOM** | Неизвестно | Видно в трейсе (снимки DOM) |

Практический пример: как выглядит отладка с трейсом. Представьте, что у вас упал тест. Вы открываете **Trace Viewer** и видите:

`1.` Шаг 1: Переход на страницу логина — скриншот страницы.

`2.` Шаг 2: Ввод логина «admin» — поле подсвечено.

`3.` Шаг 3: Ввод пароля «12345» — поле подсвечено.

`4.` Шаг 4: Клик по кнопке «Войти» — кнопка подсвечена.

`5.` Шаг 5: Ожидание появления сообщения «Добро пожаловать» — здесь ошибка!

Вы видите, что после клика по кнопке страница не загрузилась, а вместо сообщения *«Добро пожаловать»* появилась ошибка *«Сервер недоступен»*.

Вы сразу понимаете: **проблема не в тесте, а в сервере**.

Без трейса вы бы гадали: *«Может, локатор неправильный? Может, кнопка не нажалась? Может, страница не загрузилась?»*, что занимает крайне большое количество времени.
