vyvchy
    Теми розділу

    07 · Інструменти автоматизації

    Playwright: автентифікація і повторне використання стану

    Зміст

    Логін — найнудніший крок у й одночасно найдорожчий. Якщо кожен із двохсот тестів починається з відкриття форми, введення пароля й очікування редиректу, ви платите двічі: хвилинами прогону і надійністю. Хвилини очевидні. Надійність — ні: форма логіну стає спільною точкою , і будь-яке моргання на ній забарвлює червоним усю сюїту, хоч перевіряли ви зовсім інше.

    Тому «як не логінитися в кожному тесті» — стандартне питання рівня middle. Слабка відповідь звучить як «зберігаємо кукі у файл». Сильна має дві частини: механізм (логін один раз на прогін і повторне використання збереженого стану) плюс політика (що робити, коли цей стан протухне, і де він лежить, щоб не поїхати в репозиторій разом із живою сесією). Друга частина ламається першою — і ламається в нічному прогоні, коли поруч нікого немає.

    Чистий контекст — і чому стан доводиться вкладати

    Playwright виконує тести в ізольованих (browser context). Кожен тест дістає свіжий контекст, еквівалентний новому профілю браузера, і ця ізоляція коштує майже нічого. Дока називає причину прямо: така модель покращує відтворюваність і не дає падінням каскадитися одне за одним.

    Ціна цієї ізоляції — та сама властивість, що й вигода. Чистий профіль означає профіль розлогінений: немає кукі — немає сесії. Отже, автентифікований стан треба вкладати в тест. Офіційна формула звучить так: тести можуть завантажити наявний автентифікований стан, і це знімає потребу автентифікуватися в кожному тесті та прискорює виконання.

    Далі в главі під «станом» мається на увазі (storage state) — файл із кукі та сховищем автентифікованого браузера, який тести завантажують замість логіну. Механіка контекстів як така — у главі про архітектуру, браузери й контексти, а кукі й серверні сесії як явище — у Кукі, сесії та сховище браузера.

    Що збережений стан покриває, а що ні

    Це найкорисніший факт глави, бо він відповідає на половину питань «чому не працює». Повторне використання автентифікованого стану покриває кукі, localStorage, IndexedDB і автентифікацію на основі (WebAuthn).

    А от sessionStorage не покриває — API для його збереження інструмент не надає. Це не дрібниця й не недогляд: sessionStorage розділений і за , і за вкладкою, тож закриття вкладки знищує всі звʼязані з нею дані. Новий контекст його не успадкує за побудовою. Практичний наслідок: якщо застосунок тримає токен саме там, збереженим станом ви його не «залогінете»: відновлювати доводиться вручну, окремим кроком у тесті.

    Сам файл робиться одним викликом на контексті:

    // Зберегти стан автентифікованого контексту у файл
    await context.storageState({ path: 'playwright/.auth/user.json' });

    А підставляється — опцією оточення тесту:

    // playwright.config.ts — тести стартують уже автентифікованими
    use: {
      storageState: 'playwright/.auth/user.json',
    },

    Setup-проєкт: логін один раз на прогін

    Проєкт (project) у конфігурації — це логічна група тестів, що виконуються з однаковою конфігурацією. Залежності — список проєктів, які мають відпрацювати перед тестами іншого проєкту; саме так конфігурують глобальну підготовку. З цих двох деталей і складається весь рецепт: логін живе в окремому проєкті, а решта проєктів оголошують його своєю залежністю.

    Дока формулює це як рекомендований підхід для тестів без серверного стану: автентифікуватися один раз у setup-проєкті, зберегти стан і бутстрапити ним кожен тест. Умову «без серверного стану» запамʼятайте — до неї ми повернемось окремим підрозділом, бо саме її порушення дає найтиповіший клас падінь.

    storageState у файл

    setup-проєкт
    auth.setup.ts

    playwright/.auth/user.json

    проєкт chromium
    dependencies = setup

    проєкт firefox
    dependencies = setup

    тести стартують
    уже автентифікованими

    storageState у файл

    setup-проєкт
    auth.setup.ts

    playwright/.auth/user.json

    проєкт chromium
    dependencies = setup

    проєкт firefox
    dependencies = setup

    тести стартують
    уже автентифікованими

    У конфігурації це виглядає так:

    // playwright.config.ts
    export default defineConfig({
      projects: [
        { name: 'setup', testMatch: /auth\.setup\.ts/ },
        {
          name: 'chromium',
          use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
          dependencies: ['setup'],
        },
      ],
    });

    Сам setup — звичайний тест, який нічого не перевіряє, крім факту логіну:

    import { test as setup, expect } from '@playwright/test';
    
    const authFile = 'playwright/.auth/user.json';
    
    setup('authenticate', async ({ page }) => {
      await page.goto('/login');
      await page.getByLabel('Email').fill(process.env.USER_EMAIL!);
      await page.getByLabel('Password').fill(process.env.USER_PASSWORD!);
      await page.getByRole('button', { name: 'Sign in' }).click();
      // Без цієї перевірки збережеться стан гостя, і всі тести впадуть пізніше й незрозуміло
      await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
      await page.context().storageState({ path: authFile });
    });

    Перевірка перед збереженням — не формальність. Клік по кнопці не означає, що сесія зʼявилася; чекати треба на стан, а не на дію (механіка автоочікування й web-first перевірок).

    Чотири деталі механізму, які варто знати ще до першого прогону:

    • Setup-тести видно в репортері, і на них теж записується. Тобто підготовка дебажиться тими самими інструментами, що й тести, — див. дебаг, trace viewer і репортинг.
    • Провал залежності не робить тести червоними — він їх не запускає. Якщо тести залежності впали, залежні тести не виконуються взагалі. Зламаний логін дає один червоний setup і двісті не запущених тестів, а не двісті однакових фейлів.
    • Кілька залежностей виконуються паралельно — це важливо, якщо крім логіну ви піднімаєте ще щось.
    • Фільтрація тягне залежності за собою. Будь-яка фільтрація — --grep, --shard, вибір за розташуванням, test.only() — вибирає лише основні тести, а тести залежностей підтягуються самі. Тому шард на CI не лишається без логіну. Вимкнути це можна прапорцем --no-deps, і тоді ви лишаєтеся без підготовки свідомо.

    чіпляється не до кожного проєкту, а до setup-проєкту окремою властивістю teardown — і відпрацьовує після всіх залежних проєктів.

    Дрібниця, на якій спотикаються при першій спробі: конфіг має два рівні — опції самого ранера лежать нагорі, опції оточення тесту — у секції use. Дока попереджає про це прямо: опції ранера — верхнього рівня, у use їх не кладуть. А storageState — саме опція оточення. Ширший розбір конфігурації і окрема глава.

    Кілька ролей: окремі файли стану

    Коли ролей більше однієї, але акаунти можна перевикористовувати між тестами, схема не змінюється — множиться лише кількість файлів. У setup-проєкті автентифікуються кілька разів, а далі стан вказують для кожного файла або групи тестів, а не в конфізі глобально:

    // admin.spec.ts — увесь файл їде під роллю адміна
    test.use({ storageState: 'playwright/.auth/admin.json' });

    Якщо ж дві ролі потрібні в одному тесті (класика: адмін щось публікує, користувач це бачить), то це два контексти з різними станами:

    const adminContext = await browser.newContext({ storageState: 'playwright/.auth/admin.json' });
    const userContext = await browser.newContext({ storageState: 'playwright/.auth/user.json' });

    І симетричний випадок, який чомусь завжди забувають: тест незалогіненого користувача окремого проєкту не вимагає. Стан скидається на рівні файла:

    // guest.spec.ts — гостьові сценарії: скидаємо стан, який поставив проєкт
    test.use({ storageState: { cookies: [], origins: [] } });

    Спільний акаунт чи акаунт на воркер

    Тепер повернімось до умови, яку легко проґавити. Один файл стану на всю сюїту дока рекомендує для тестів, які не змінюють серверний стан. Для тестів, які його змінюють, рекомендація інша: кожен паралельний автентифікується один раз під власним акаунтом, усі тести цього воркера перевикористовують той самий стан, а розрізняють воркери за testInfo.parallelIndex. Акаунтів, відповідно, потрібно стільки, скільки воркерів.

    Причина — в моделі паралельності. Воркер (worker) — це процес операційної системи зі своїм браузером; спільного стану й глобальних змінних між воркерами немає, а одиниця паралельності за замовчуванням — файл, а не тест. Отже, два тести на одному акаунті цілком легально їдуть одночасно в різних процесах: один видаляє сутність, поки другий її редагує. Симптом класичний — «поодинці зелені, разом червоні», і виглядає він як , хоча це гонка за даними (див. боротьбу з флаком засобами інструмента і главу про воркери й шардінг (sharding)).

    Ні

    Так

    Тести змінюють
    серверний стан?

    Один акаунт на прогін
    setup-проєкт + storageState

    Окремий акаунт
    на кожен воркер

    Автентифікація раз на воркер,
    розрізнення за parallelIndex

    Ні

    Так

    Тести змінюють
    серверний стан?

    Один акаунт на прогін
    setup-проєкт + storageState

    Окремий акаунт
    на кожен воркер

    Автентифікація раз на воркер,
    розрізнення за parallelIndex

    Технічно «раз на воркер» — це воркерна фікстура (fixture): тестова фікстура прибирається після кожного тесту, а воркерна — лише коли завершується сам воркер. Дорога автентифікація — типовий кандидат саме на воркерний : «акаунт на воркер» і реалізують воркерною фікстурою. Є й нюанс: воркер перевикористовується для наступних файлів лише поки воркерні фікстури збігаються, тож роль, вшита у воркерну фікстуру, впливає на розкладку файлів по воркерах.

    Стратегія та ізоляції загалом — не тема цієї глави: канон живе в розділі «Автоматизація: стратегія». Тут важливо лише те, що акаунт — це теж тестові дані, і спільний акаунт у тестах, які правлять стан, — червоний на рев'ю.

    Протухлий стан

    Головне про протухання формулюється одним реченням: інструмент за ним не стежить. Дока прямо кладе це на автора тестів — збережений стан треба видалити, коли він протух. Ніякої автоматичної перевірки «а ця сесія ще жива?» перед прогоном немає, і файл із мертвою кукою виглядає точно так само, як із живою.

    Звідси два штатні рішення. Перше: якщо стан між прогонами не потрібен, писати його в теку виводу проєкту (testProject.outputDir), яка автоматично чиститься перед кожним прогоном — тоді логін просто відбувається щоразу заново. Друге стосується локальної роботи: UI-режим за замовчуванням не виконує setup-проєкт (щоб не гальмувати), тож автентифікацію там оновлюють, час від часу запускаючи auth.setup.ts руками.

    Чому стан протухає взагалі — механіка з боку сервера, і вона не інструментальна. Сесійна кукі не має ні Expires, ні Max-Age і зникає із браузера, а постійна живе до вказаного моменту; сервер, який видав сесію, може обірвати її раніше з власних причин. Розбір цієї частини — у главі про кукі й сесії та автентифікацію й авторизацію.

    Цікаво порівняти з іншим підходом: у CodeceptJS та сама задача закрита плагіном — сесія зберігається в памʼять або файл, а протухлу сесію плагін перелогінює сам. Це архітектурний вибір, а не перевага: Playwright віддає політику протухання вам і не робить нічого за вашою спиною, CodeceptJS бере її на себе разом із наслідками (див. CodeceptJS: актор, хелпери і сценарний стиль).

    Логін через API замість UI

    Проклацувати форму логіну в setup — найпростіший, але не єдиний шлях. Контекст API-запитів (APIRequestContext) вміє надсилати будь-які HTTP(S)-запити мережею, і офіційна дока сама перелічує три його застосування: тестувати серверне API, готувати серверний стан перед відкриттям застосунку в тесті й перевіряти серверні пост-умови після дій у браузері.

    Ключове твердження, на якому тримається весь прийом: стан сховища взаємозамінний між браузерним контекстом і контекстом API-запитів. Тобто офіційно підтримуваний шлях — залогінитися , а тоді створити контекст, у якому кукі вже є. У доці автентифікації це той самий рецепт іншими словами: надіслати запит через APIRequestContext і зберегти автентифікований стан, як завжди.

    import { test as setup } from '@playwright/test';
    
    setup('authenticate via API', async ({ request }) => {
      await request.post('/api/session', {
        data: { email: process.env.USER_EMAIL, password: process.env.USER_PASSWORD },
      });
      await request.storageState({ path: 'playwright/.auth/user.json' });
    });

    Виграш очевидний: логін перестає залежати від верстки форми, від на стенді, від редиректів SSO. Два нюанси, які варто знати:

    • Контексти API-запитів бувають двох типів. Той, що доступний через browserContext.request і page.request, заповнює заголовок Cookie з браузерного контексту й сам оновлює кукі браузера, коли у відповіді є Set-Cookie. Окремо створений екземпляр має власне ізольоване сховище кукі. Різниця вирішує, чи «побачить» браузер результат вашого запиту.
    • Фікстура request успадковує конфігbaseURL, extraHTTPHeaders, а також ; під капотом це виклик створення нового контексту запитів, який можна зробити й вручну, якщо потрібен повний контроль.

    І висновок, який із цього прийому не випливає: обхід UI-логіну не скасовує самого UI-логіну як функціональності продукту. Хоч один тест мусить проходити форму по-справжньому — інакше вхідна точка, через яку в застосунок заходять усі користувачі, не покрита нічим. Ширше про роботу з мережею й підготовку даних — у главі про перехоплення, мокання й очікування відповідей; API як предмет тестування — окремий розділ «API-тестування».

    Що не можна класти в репозиторій

    Файл стану — це не конфігурація. Це живий секрет: у ньому лежать чутливі кукі й заголовки, якими можна вдати вас або ваш тестовий акаунт. Дока не радить «бути обережним», а наполегливо не радить такі файли в приватні чи публічні репозиторії. Слово «приватні» тут несе половину змісту: приватність репозиторію не перетворює живу сесію на конфіг. Штатна практика — окрема тека playwright/.auth, додана до .gitignore.

    Далі — три речі, які легко зробити неправильно.

    .gitignore не лікує вже закомічене. Його дока формулює межу дослівно: файли, які Git уже відслідковує, правило не зачіпає. Щоб перестати відслідковувати доданий файл, потрібен git rm --cached — прибрати його з індексу, і лише тоді патерн у .gitignore не дасть файлу вернутися в наступні коміти.

    Прибрати з індексу — не те саме, що відкликати доступ. Секрет, який уже потрапив у історію (або в лог прогону), скомпрометований. Припис GitHub для секрету, що засвітився в лозі прогону, прямий: видалити лог і ротувати секрет; регулярна ротація ще й скорочує вікно, у якому скомпрометований секрет ще діє. Практичний порядок дій — спершу ротація (тільки вона відкликає доступ), потім прибирання з місця, де секрет засвітився, і лише потім думки про історію.

    Креденшели беруться із середовища, а не з коду. Лакмусовий тест на це є в дванадцятифакторній методології: чи можна зробити кодову базу опенсорсною будь-якої миті, не скомпрометувавши жодного креденшела. Конфіг-файл поза контролем версій — велике поліпшення проти констант у коді, але слабкість лишається: такий файл легко випадково закомітити.

    Два факти про CI, які ламають інтуїцію й прямо стосуються цієї теми. По-перше, будь-хто з правом запису в репозиторій має право читати всі його секрети — тож «тестовий акаунт із доступом до всього» неявно роздає цей доступ усій команді, що може пушити. По-друге, за винятком GITHUB_TOKEN, секрети не передаються ранеру, коли запущений із форку: e2e на PR із форку падає на автентифікації не через баг , а через документовану поведінку. Механіка секретів, маскування і його межі — канон розділу «Git і CI/CD».

    Типові помилки

    • «Логін проходить, а наступна сторінка бачить гостя». Виглядає як баг застосунку, а насправді токен живе в sessionStorage: API для його збереження інструмент не надає, та й саме сховище привʼязане до вкладки. Стан треба відновлювати вручну, окремим кроком у тесті.
    • «У UI-режимі все розлогінене, а в консолі зелено». Виглядає як зламаний конфіг, а насправді UI-режим за замовчуванням не запускає setup-проєкт. Лікується запуском auth.setup.ts руками.
    • «Нічний прогін червоний увесь, локально зелено». Виглядає як проблема оточення, а насправді протух файл стану, і ніхто про це не дізнався: за протуханням інструмент не стежить.
    • «Тести падають тільки в паралелі». Виглядає як , а насправді порушена умова: спільний акаунт рекомендований для тестів без серверного стану, а ваші його змінюють. Лікування — акаунт на воркер, не .
    • «Шард на CI упав, бо в ньому не було логіну». Виглядає як гонка залежностей, а насправді залежності підтягуються за будь-якої фільтрації, включно з --shard. Якщо логіну справді не було — або прогін ішов із --no-deps, або setup узагалі не оголошений залежністю в конфізі.
    • «Файл стану закомітили, потім додали в .gitignore — полагодили». Виглядає як полагоджено, а насправді правило не чіпає вже відслідковувані файли, а секрет усе одно лежить в історії. Працює лише ротація.
    • «Тест на форму логіну падає, бо вже залогінений». Виглядає як баг тесту, а насправді проєкт роздає стан усім своїм тестам. Для гостьових сценаріїв стан скидають на рівні файла.

    Підсумок

    • Чистий контекст — це розлогінений контекст. Ізоляція, яка дає відтворюваність, і є причиною, чому стан доводиться вкладати в тест явно.
    • Збережений стан покриває кукі, localStorage, IndexedDB і passkey — і не покриває sessionStorage. Це перше, що перевіряють, коли «стан підхопився, а застосунок вважає користувача гостем».
    • «Логін раз на прогін» — це setup-проєкт, оголошений залежністю, а не beforeAll у кожному файлі. Залежності підтягуються навіть при фільтрації й , а провал підготовки не запускає залежні тести.
    • Один акаунт на всіх легальний лише для тестів, які не змінюють серверний стан. Щойно тести правлять дані — акаунт на кожен воркер, бо воркер це окремий процес без спільного стану.
    • Протухання — ваша відповідальність, а файл стану — секрет. Політика («де лежить, коли перегенерується, що робити при витоку») — така сама частина рішення, як і сам storageState.

    Можливі питання

    • «Як ви не логінитеся в кожному тесті?» — інтерв'юер чекає не назви API, а схеми: логін один раз у setup-проєкті, збереження стану у файл, підстановка стану проєктам через залежності. Згадка про протухання одразу відрізняє того, хто це запускав, від того, хто про це читав.
    • «Що потрапляє в storageState, а що ні?» — перевіряють конкретику. Правильна відповідь називає кукі, localStorage, IndexedDB і passkey — і окремо те, що sessionStorage не зберігається, тому застосунки, які тримають токен там, вимагають іншого рішення.
    • «У вас тести під двома ролями — як організуєте?» — очікують «кілька файлів стану з одного setup-проєкту, роль вибирається на рівні файла або групи тестів», а для сценарію «адмін і користувач в одному тесті» — два контексти з різними станами.
    • «Тести на спільному акаунті падають у паралелі. Що робите?» — дивляться, чи бачите ви за симптомом гонку, а не флак. Сильна відповідь називає межу застосовності спільного акаунта й перехід на акаунт-на-воркер, а не збільшення кількості ретраїв.
    • «Де у вас лежать креденшели й файл стану?» — це питання про гігієну. Слабка відповідь — «у репозиторії, він же приватний». Очікують: пароль зі змінних середовища або секретів CI, стан у теці під .gitignore, при витоку — ротація, бо .gitignore вже закомічене не лікує.
    • «Логінитеся через UI чи через API?» — правильна відповідь ставить обидва варіанти на місце: у setup швидше й стабільніше через API (стан взаємозамінний між API- і браузерним контекстом), але сам UI-флоу логіну лишається під тестом хоча б в одному сценарії.

    Джерела

    Чистий контекст — і чому стан доводиться вкладати

    • Playwright — Authentication — ізоляція контекстів як причина відтворюваності й завантаження готового стану замість логіну в кожному тесті.
    • Playwright — головна сторінка — свіжий контекст на тест як еквівалент нового профілю браузера з ізоляцією майже без накладних витрат.

    Що збережений стан покриває, а що ні

    • Playwright — Authentication — що саме покриває повторне використання стану і чому sessionStorage до нього не входить.
    • MDN — Web Storage APIsessionStorage розділений за вкладкою і походженням, тож дані гинуть із вкладкою.

    Setup-проєкт: логін один раз на прогін

    • Playwright — Projects — проєкт як логічна група з однаковою конфігурацією, залежності проєктів, поведінка при провалі залежності, teardown і те, що фільтрація підтягує залежності.
    • Playwright — Authentication — setup-проєкт як рекомендований підхід для тестів без серверного стану й умова застосовності.
    • Playwright — Test configuration — два рівні конфігурації: опції ранера проти опцій оточення в секції use.

    Кілька ролей: окремі файли стану

    • Playwright — Authentication — кілька автентифікацій в одному setup-проєкті, вибір стану на рівні файла чи групи тестів і скидання стану для гостьових сценаріїв.
    • Playwright — Test Isolation (browser contexts) — кілька контекстів із різними станами в одному тесті.

    Спільний акаунт чи акаунт на воркер

    • Playwright — Authentication — окремий акаунт на воркер для тестів, що змінюють серверний стан, автентифікація раз на воркер і розрізнення за parallelIndex.
    • Playwright — Parallelism — воркер як процес ОС без спільного стану й файл як одиниця паралельності за замовчуванням.
    • Playwright — Test fixtures: тестова прибирається після тесту, воркерна — з завершенням воркера.

    Протухлий стан

    • Playwright — Authentication — протухання як обовʼязок автора тестів, тека виводу проєкту як спосіб не тримати стан між прогонами, UI-режим без setup-проєкту.
    • CodeceptJS — Plugins — сесія у файлі чи памʼяті й автоматичний перелогін після протухання.
    • MDN — Using HTTP cookies — сесійна кукі без Expires/Max-Age проти постійної з терміном життя.

    Логін через API замість UI

    • Playwright — API testing — три застосування контексту API-запитів, взаємозамінність стану між браузерним і API-контекстом, два типи контексту й успадкування опцій фікстурою request.
    • Playwright — Authentication — автентифікація запитом до API зі збереженням стану у файл, як після UI-логіну.

    Що не можна класти в репозиторій

    • Playwright — Authentication — файл стану містить чутливі кукі й заголовки; наполеглива порада не комітити його й тримати в теці з .gitignore.
    • Git Reference — gitignore — правило не зачіпає вже відслідковувані файли; git rm --cached як спосіб прибрати файл з індексу.
    • GitHub Docs — Secure use reference (GitHub Actions) — видалити лог і ротувати секрет після витоку; ротація скорочує вікно дії; право запису дає право читати всі секрети.
    • GitHub Docs — Using secrets in GitHub Actions — секрети не передаються ранеру для воркфлоу з форку, крім GITHUB_TOKEN.
    • The Twelve-Factor App — III. Config — лакмусовий тест на відкриття кодової бази й межа конфіг-файлів поза контролем версій.

    Типові помилки

    • Playwright — AuthenticationsessionStorage поза станом, UI-режим без setup-проєкту, протухання як обовʼязок автора, умова спільного акаунта, скидання стану на рівні файла.
    • Playwright — Projects — фільтрація (включно з --shard) підтягує тести залежностей; --no-deps це вимикає.
    • MDN — Web Storage API — привʼязка sessionStorage до вкладки й походження.
    • Git Reference — gitignore — межа правила для вже відслідковуваних файлів.

    Можливі питання

    • Playwright — Authentication — склад збереженого стану, кілька ролей і файлів стану, умова спільного акаунта проти акаунта на воркер, гігієна файлу стану, логін через API.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.