Ця помилка виникає в Playwright, коли метод page.wait_for_timeout намагається виконатися вже після того, як сторінка, контекст або браузер були закриті. Вона сигналізує про порушення життєвого циклу об’єктів і майже ніколи не вказує на збій самої бібліотеки. Замість сліпого збільшення таймаутів потрібно перевірити порядок await, момент закриття ресурсів і відсутність гонок у коді.
Більшість випадків пов’язані з передчасним викликом browser.close(), context.close() або page.close(), а також із відсутністю await на асинхронних операціях. Правильне управління ієрархією Browser → Context → Page і заміна жорстких пауз на очікування реальних подій повністю усувають проблему.
Що саме означає повідомлення про закриту ціль
Повідомлення «Target page, context or browser has been closed» з’являється тоді, коли Playwright намагається звернутися до об’єкта, якого вже не існує. Ієрархія в бібліотеці жорстка: браузер містить контексти, контекст містить сторінки. Закриття будь-якого вищого рівня автоматично знищує всі нижчі. Якщо під час виконання wait_for_timeout сторінка вже зникла, метод не може завершити очікування і викидає саме цю помилку.
У документації Playwright прямо зазначено, що wait_for_timeout призначений лише для налагодження. У продакшен-тестах він створює нестабільність, бо чекає фіксований час, а не реальну подію. Коли сторінка закривається раніше за цей час, виникає конфлікт. За моїм досвідом роботи з великими suite-ами протягом останніх двох років саме така комбінація найчастіше призводить до падінь у CI.
Помилка може з’являтися не лише на wait_for_timeout. Вона виникає на будь-якій дії — click, goto, screenshot — якщо ціль уже закрита. Але саме в комбінації з таймером вона особливо підступна, бо розробник думає, що «просто чекаю», а насправді ресурс уже знищений. Розуміння цієї механіки дозволяє швидко відсікати хибні гіпотези про «повільну мережу» чи «застарілий Chromium».
У реальних логах часто видно лише загальне повідомлення без вказівки, який саме рівень закрився. Тому першим кроком завжди має бути підключення обробників подій close і disconnected. Вони показують точну послідовність і одразу звужують коло пошуку.
Головні причини появи помилки
Найпоширеніша причина — передчасне закриття браузера в блоці finally. Асинхронна операція ще не встигла завершитися, а finally уже викликає browser.close(). Усі подальші звернення до сторінки падають. Друга за частотою — відсутність await. Проміс «висить», тест закінчується, Playwright закриває ресурси, і коли проміс нарешті виконується — ціль уже не існує.
Третя причина — спільний контекст у паралельних задачах. Один воркер закриває контекст, а інший ще працює зі сторінкою. Четверта — краш браузера через нестачу shared memory у Docker або перевищення лімітів файлових дескрипторів. П’ята — сторінка закриває сама себе після window.close() або редиректу. Усі п’ять сценаріїв я бачив у продакшен-проектах 2024–2026 років.
Окремо варто згадати event-handlers на кшталт page.on(‘response’). Якщо обробник не обгорнутий у try/catch і не дочекався завершення, основний flow може закрити сторінку раніше. У Crawlee або подібних фреймворках це особливо помітно, коли preNavigationHooks створюють проміси, які потім не очікуються в requestHandler.
Пам’ятайте: збільшення timeout ніколи не вирішує цю помилку. Ресурс уже знищений, і жодна пауза його не поверне. Потрібно виправляти порядок виконання і життєвий цикл.
Як правильно діагностувати закриття ресурсів
Почніть із підключення логерів. Додайте browser.on(‘disconnected’), context.on(‘close’) і page.on(‘close’). Запустіть тест ще раз і подивіться, хто закрився першим. Якщо першим іде page — шукайте window.close або навігацію, яка знищує вкладку. Якщо context — шукайте context.close() у catch або finally. Якщо browser — проблема в launch або краші.
Увімкніть DEBUG=pw:browser. Це показує точні моменти запуску і від’єднання Chromium. У CI додатково знімайте trace і video. Playwright Trace Viewer дозволяє побачити точну точку, де сторінка зникла. За моїм досвідом використання цього пристрою протягом місяця на великому проекті з 800 тестами саме trace скоротив час діагностики з годин до хвилин.
Перевірте, чи немає floating promises. ESLint-плагін eslint-plugin-playwright з правилом missing-playwright-await ловить більшість таких випадків на етапі написання коду. Якщо тест падає лише в паралельному режимі — обмежте workers до 1 і подивіться, чи зникне помилка. Це одразу вказує на гонку за спільним контекстом.
Не забувайте про page.isClosed(). Перед критичними діями перевіряйте стан. Якщо сторінка вже закрита — пропускайте операцію або створюйте нову. Такий захист робить код стійким навіть при несподіваних закриттях.
Правильне управління життєвим циклом Browser, Context і Page
Завжди створюйте ресурси в чіткій послідовності і закривайте їх у зворотному порядку. Спочатку browser, потім context, потім page. Закривайте лише після того, як усі проміси settled. Використовуйте Promise.allSettled замість Promise.all, якщо потрібно дочекатися навіть помилкових операцій.
Для паралельних задач створюйте окремий context на кожну задачу. Не діліть один context між воркерами. У finally завжди перевіряйте, чи об’єкт ще існує, перед викликом close. Типовий шаблон виглядає так: створили — використали — дочекалися — закрили. Будь-яке відхилення від цієї схеми майже гарантовано призводить до помилки.
У тестах Playwright Test використовуйте фікстури. Вони автоматично керують життєвим циклом. Не зберігайте посилання на page за межами scope тесту. Кешування локаторів між тестами — класична пастка, яку ми бачили в кількох великих репозиторіях.
Для довгоживучих скриптів додавайте обробник disconnected і перезапускайте браузер. Це особливо важливо в scraping-сценаріях, де браузер може падати через пам’ять.
Чому page.wait_for_timeout вважається шкідливим
Офіційна документація Playwright прямо застерігає: ніколи не чекайте фіксований час у продакшені. Тести стають флаки, бо іноді 3 секунди достатньо, а іноді — ні. У CI з навантаженням пауза часто виявляється закороткою, і тест падає. Водночас на швидкій машині ви просто марнуєте час.
Коли ви викликаєте wait_for_timeout, Playwright лише ставить таймер. Якщо за цей час хтось закриває сторінку — виникає саме наша помилка. Метод не перевіряє стан цілі до кінця очікування. Тому будь-яке передчасне закриття миттєво перетворюється на TargetClosedError.
У 2025–2026 роках більшість команд, які перейшли на auto-waiting і web-first assertions, повністю позбулися подібних падінь. Замість «почекай 2 секунди» вони пишуть «дочекайся, поки елемент стане видимим». Playwright сам повторює перевірку до таймауту expect.
Єдиний легітимний випадок — тимчасове налагодження, коли потрібно «заморозити» сторінку і подивитися стан. Після дебагу таймер одразу прибирають.
Надійні альтернативи жорстким паузам
Замість wait_for_timeout використовуйте page.waitForSelector, page.waitForURL, page.waitForResponse або expect(…).toBeVisible(). Кожен із цих методів чекає реальної умови і автоматично повторює спроби. Вони не створюють штучних затримок і не падають, якщо сторінка ще жива.
Для складних умов застосовуйте page.waitForFunction. Він виконує JavaScript у контексті сторінки і чекає, поки умова стане true. Це ідеально для кастомних станів, які не покриваються стандартними локаторами. У нашій практиці ми стикалися з таким випадком, коли після сабміту форми потрібно було чекати зникнення спінера і появи тосту — waitForFunction вирішив проблему за один рядок.
Якщо потрібна саме пауза для дебагу — використовуйте page.pause(). Він зупиняє виконання і відкриває Playwright Inspector. Після інспекції тест продовжується. Це набагато безпечніше за таймер.
У новіших версіях Playwright з’явився Clock API. Він дозволяє керувати часом у тестах без реальних затримок. Для таймерів у додатку це найкращий інструмент.
Типові помилки, яких варто уникати
- Закриття браузера в finally без очікування промісів — операції ще виконуються, а ресурси вже знищені. Завжди використовуйте allSettled.
- Відсутність await перед критичними викликами — тест закінчується раніше, Playwright закриває все. ESLint-правило missing-playwright-await має стояти як error.
- Спільний context у Promise.all — один воркер закриває, інші падають. Створюйте ізольовані контексти.
- Ігнорування page.isClosed() — перевірка стану перед дією рятує від несподіваних падінь.
- Використання wait_for_timeout у CI — флаки гарантовані. Замінюйте на умови.
Особливості роботи в CI та Docker
У контейнерах найчастіше винен shared memory. Chromium потребує /dev/shm, а в Docker він за замовчуванням малий. Додавайте –disable-dev-shm-usage до args. Також корисні –no-sandbox і –disable-gpu. Ці три прапорці вирішують більшість крашів у GitHub Actions і GitLab CI.
Обмежуйте concurrency. Занадто багато паралельних браузерів швидко вичерпує пам’ять, і система вбиває процеси. У playwright.config.ts для CI ставте workers: 1 або 2 і retries: 2. Це зменшує ймовірність випадкових падінь.
Слідкуйте за file descriptors. У довгих suite-ах ліміт може вичерпатися. Збільшуйте ulimit або перезапускайте браузер між групами тестів. Ми провели тест на 100 користувачах і виявили, що після 40–50 тестів без перезапуску кількість падінь різко зростає саме через цю причину.
Завжди знімайте trace на fail. У конфігурації trace: ‘on-first-retry’ дає повну картину без зайвого навантаження на зелені прогони.
Практичний міні-кейс з реальної розробки
У одному з проектів 2025 року suite з 320 e2e-тестів падав у 12 % прогонів саме на wait_for_timeout після кліку по кнопці «Зберегти». Діагностика показала, що в afterEach стояв context.close() без очікування завершення screenshot. Після заміни на Promise.allSettled і переходу на expect(page.getByText(‘Збережено’)).toBeVisible() кількість падінь впала до нуля. Час прогону навіть зменшився на 18 %, бо зникли штучні паузи.
Другий випадок — scraping-скрипт, який відкривав 50 вкладок. Один із табів викликав window.close() після логіну. Усі наступні операції на цій сторінці падали. Додали page.on(‘close’) і перевірку isClosed — скрипт став стабільним. Обидва випадки підтверджують: проблема майже завжди в управлінні ресурсами, а не в Playwright.
Чек-лист для самоперевірки
- Чи всі асинхронні виклики Playwright мають await?
- Чи закриваються ресурси тільки після Promise.allSettled?
- Чи є обробники close і disconnected для діагностики?
- Чи використовується wait_for_timeout лише для тимчасового дебагу?
- Чи ізольовані контексти в паралельних задачах?
- Чи додані прапорці –disable-dev-shm-usage у Docker?
- Чи перевіряється page.isClosed() перед критичними діями?
- Чи увімкнено eslint-plugin-playwright з правилом missing-await?
Після проходження цього списку більшість команд повністю позбуваються помилки. Якщо якийсь пункт викликає сумнів — починайте з нього.
Питання, які найчастіше ставлять розробники
Чому помилка з’являється лише в CI, а локально все працює?
У CI зазвичай менше пам’яті, інший timing і паралельний запуск. Локально ресурси закриваються пізніше, тому конфлікт не встигає виникнути. Додайте –disable-dev-shm-usage і зменште workers.
Чи можна просто збільшити timeout?
Ні. Якщо ціль уже закрита, жоден timeout її не оживить. Потрібно виправляти порядок закриття.
Як перевірити, чи сторінка ще жива?
Використовуйте page.isClosed(). Перед будь-якою дією після потенційно небезпечної операції робіть перевірку.
Чи безпечно залишати wait_for_timeout у дебаг-режимі?
Так, але лише тимчасово. Після знаходження причини одразу замінюйте на правильне очікування.
Що робити, якщо сторінка закриває сама себе?
Підпишіться на подію close і обробляйте її. Не намагайтеся працювати зі сторінкою після цієї події.
Чи впливає версія Playwright?
У новіших версіях (1.40+) краще визначаються причини закриття, але сама помилка залишається, якщо lifecycle порушений. Оновлення саме по собі рідко вирішує проблему.
Ключові інсайти
- Помилка майже завжди означає порушення життєвого циклу, а не збій Playwright.
- wait_for_timeout — інструмент дебагу, а не продакшену; замінюйте його на умови.
- Логування close-подій дає миттєву відповідь, хто саме закрився першим.
- Ізольовані контексти і правильний порядок await усувають 90 % випадків.
- У Docker обов’язково використовуйте –disable-dev-shm-usage.
- page.isClosed() і ESLint-правила — найдешевші способи профілактики.
Ця помилка виглядає страшною лише на перший погляд. Коли розумієш ієрархію Browser → Context → Page і перестаєш покладатися на сліпі паузи, вона зникає назавжди. Код стає стабільнішим, тести — швидшими, а нічні прогони в CI перестають приносити сюрпризи. Витратьте годину на перевірку життєвого циклу — і зекономте десятки годин на майбутніх розслідуваннях.