Почему Puppeeter заставляет мой набор тестов зависать на 30 секунд, когда я использую «waitForSelector», даже если я вызываю «закрыть» на странице и в браузере?

У меня есть набор тестов Node.js Mocha (я создал минимальное воспроизведение на основе реального приложения, для которого пытался создать автоматизированный тест).

package.json:

{
  "name": "puppeteer-mocha-hang-repro",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "chai": "4.3.7",
    "express": "4.18.2",
    "mocha": "10.2.0",
    "puppeteer": "19.6.2"
  }
}

index.spec.js:

const expect = require('chai').expect;
const express = require('express');
const puppeteer = require('puppeteer');

const webServerPort = 3001;

describe('test suite', function () {
    this.timeout(10000);

    let webServer;
    let browser;

    beforeEach(async () => {
        // Start web server using Express
        const app = express();
        app.get('/', (_, res) => {
            res.send('<html>Hello, World from the <span id = "source">Express web server</span>!</html>');
        });
        webServer = app.listen(webServerPort, () => {
            console.info(`Web server listening on port ${webServerPort}.`);
        });

        // Start browser using Puppeteer
        browser = await puppeteer.launch();
        console.info('Browser launched.');
    });

    afterEach(async () => {
        // Stop browser
        await browser.close();
        console.info('Browser closed.');

        // Stop web server
        await webServer.close();
        console.info('Web server closed.');
    });

    it('should work', async () => {
        const page = await browser.newPage();

        await page.goto(`http://localhost:${webServerPort}/`);
        console.info('Went to root page of web server via Puppeteer.');

        if (process.env['PARSE_PAGE'] === 'true') {
            const sel = await page.waitForSelector('#source');
            const text = await sel.evaluate(el => el.textContent);
            console.info('According to Puppeteer, the text content of the #source element on the page is:', text);
            expect(text).eql('Express web server');
        }

        await page.close();
        console.info('Page closed.');
    });
});

Если я запускаю набор тестов с помощью команды npx mocha index.spec.js, которая приводит к пропуску строк 45-48, набор тестов проходит, и процесс Mocha быстро завершается:

$ time npx mocha index.spec.js


  test suite
Web server listening on port 3001.
Browser launched.
Went to root page of web server via Puppeteer.
Page closed.
    ✔ should work (70ms)
Browser closed.
Web server closed.


  1 passing (231ms)


real    0m0.679s
user    0m0.476s
sys     0m0.159s

Обратите внимание, что он закончился за 0,679 с.

Если я вместо этого запущу его с помощью команды PARSE_PAGE=true npx mocha index.spec.js, которая не приведет к пропуску моего кода, тесты пройдут быстро, но процесс зависнет примерно на 30 секунд:

$ time PARSE_PAGE=true npx mocha index.spec.js


  test suite
Web server listening on port 3001.
Browser launched.
Went to root page of web server via Puppeteer.
According to Puppeteer, the text content of the #source element on the page is: Express web server
Page closed.
    ✔ should work (79ms)
Browser closed.
Web server closed.


  1 passing (236ms)


real    0m30.631s
user    0m0.582s
sys     0m0.164s

Обратите внимание, что он закончился за 30,631 с.

Я подозревал, что это означает, что я оставляю вещи открытыми, забывая вызывать такие функции, как close. Но я звоню close на веб-сервер Express, в браузер Puppeteer и на страницу Puppeteer. Я пробовал вызывать close для объектов, которые я использую, когда я не пропускаю ни один из этих кодов, то есть sel и text. Но если я попытаюсь это сделать, я получу сообщение об ошибке, говорящее мне, что эти объекты не имеют таких функций.

Детали системы:

$ node --version
v18.13.0
$ npm --version
9.4.0
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
Codename:       jammy
$ uname -r
5.10.16.3-microsoft-standard-WSL

добавьте некоторый оператор печати, чтобы выяснить, какая функция работает медленно.

chovy 31.01.2023 03:30

В моем коде уже есть операторы console.info после каждой операции, которая делает что-то значимое. Последний — «Веб-сервер закрыт». который печатается после закрытия веб-сервера Express в хуке afterEach. Я понимаю, что это означает, что каждая из функций, которые я определил в своем коде, выполнялась быстро (хук beforeEach, хук afterEach, мой тест it и мой набор describe).

Matt Welke 31.01.2023 03:33

есть задержка 30 секунд или они все печатают быстро?

chovy 31.01.2023 10:38

Все строки console.info в моем коде печатаются быстро. Для завершения всего процесса Mocha требуется около 30 секунд (после чего он заканчивается с кодом состояния 0).

Matt Welke 31.01.2023 16:17

попробуйте указать тайм-аут здесь: const sel = await page.waitForSelector('#source');

chovy 31.01.2023 16:55
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
5
93
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я не уверен, насколько это будет полезно, но вы можете попробовать это:

if (process.env['PARSE_PAGE'] === 'true') {
   const sel = await page.waitForSelector('#source');
   const text = await page.evaluate(el => el.textContent, sel);
   console.info('According to Puppeteer, the text content of the #source element on the page is:', text);
   expect(text).eql('Express web server');
}

Кроме того, проверьте наличие глобальных хуков!

Это не помогло. Можете ли вы уточнить, что вы подразумеваете под глобальными хуками? Вы имеете в виду что-то конкретное либо для Puppeteer, либо для Mocha, либо для Node.js в целом?

Matt Welke 31.01.2023 16:19

У меня нет большого опыта работы с Puppeteer, но я некоторое время пользовался Playwright. В Playwright вы можете создать файл global-setup.js/ts для настройки действий до и после тестирования.

jkalandarov 31.01.2023 18:00

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

jkalandarov 31.01.2023 18:03

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

ggorlen 31.01.2023 18:04

Я ценю предложение повторно использовать браузер в нескольких тестах. Это может быть полезно для тех, кто сталкивается с этим вопросом. В моем случае я специально использовал новый браузер для каждого теста, потому что, на мой взгляд, это устраняло еще один класс проблем (повторно используемое состояние браузера). Моя цель публикации этого состояла в том, чтобы иметь минимальное воспроизведение. Моя цель позже будет состоять в том, чтобы оптимизировать мой набор тестов, как только я решу эту проблему.

Matt Welke 31.01.2023 18:41
Ответ принят как подходящий

Обновление: это поведение является регрессией, исправленной #9612 и развернутой как 19.6.3. Чтобы решить эту проблему, обновитесь до 19.6.3 (или откатитесь до <= 19.6.0, если по какой-то причине вы используете более старый Puppeteer).

Смотрите оригинальный ответ ниже.


У меня получается воспроизвести зависание даже без мокко. Кажется, это ошибка в версиях Puppeteer 19.6.1 и 19.6.2. Вот минимальный пример:

const puppeteer = require("puppeteer"); // 19.6.1 or 19.6.2

const html = `<!DOCTYPE html><html><body><p>hi</p></body></html>`;

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();
  await page.setContent(html);
  const el = await page.waitForSelector("p");
  console.info(await el.evaluate(el => el.textContent));
})()
  .catch(err => console.error(err))
  .finally(async () => {
    await browser?.close();
    console.info("browser closed");
  });

Виновником является page.waitForSelector, который, кажется, запускает свой полный 30-секундный тайм-аут по умолчанию даже после разрешения, каким-то образом предотвращая выход процесса. Я открыл выпуск #9610 в репозитории Puppeteer на GitHub.

Возможные обходные пути:

  • Откат до 19.6.0.
  • Избегайте использования waitForSelector, так как нужные вам данные находятся в статическом HTML (хотя это может не относиться к вашей фактической странице).
  • Вызов с помощью page.waitForSelector("#source", {timeout: 0}), который, кажется, решает проблему, с риском зависания навсегда, если используется в скрипте (это не проблема с мокко, так как время теста истекает).
  • Вызов с page.waitForSelector("#source", {timeout: 1000}), который уменьшает влияние задержки с риском ложного срабатывания, если загрузка элемента занимает больше секунды. Похоже, что это не суммируется, поэтому, если вы используете 1-3-секундную задержку во многих тестах, mocha должен выйти в течение нескольких секунд после завершения всех тестов, а не сумма всех задержек во всех вызовах waitForSelector. Однако в большинстве скриптов это непрактично.
  • Беги npx mocha --exit index.spec.js. Не рекомендуется - это подавляет проблему.

Я не уверен, является ли поведение специфичным для waitForTimeout или может применяться к другим методам waitFor-семейства.

Кроме того, ваш сервер прослушивает и закрывает вызовы, которые технически являются условиями гонки, поэтому:

await new Promise(resolve =>
  webServer = app.listen(webServerPort, () => {
    console.info(`Web server listening on port ${webServerPort}.`);
    resolve();
  })
);

и

await new Promise(resolve => webServer.close(() => resolve()));

Детали системы:

$ node --version
v18.7.0
$ npm --version
9.3.0
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:    22.04
Codename:   jammy
$ uname -r
5.15.0-56-generic

Я также подтвердил поведение в Windows 10.

Спасибо, что попробовали мой код и воспроизвели зависание. Ваше предложение использовать статический HTML-код страницы не сработает, когда я решу эту проблему, потому что в моем случае это просто минимальное воспроизведение с целью публикации этого в Stack Overflow, а позже я буду использовать DOM гораздо больше, ожидание событий и т. д., чтобы создать набор тестов для моих реальных приложений (которые включают в себя внутренний сервер Express, интерфейсный SPA и сторонний сервер, с которым взаимодействуют как внутренний, так и внешний интерфейс) .

Matt Welke 31.01.2023 18:46

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

Matt Welke 31.01.2023 18:47

Да, я упоминал об этом в посте. Это одно из четырех предложений, добавленных только для полноты вариантов использования, которым не нужно ждать. Вы можете игнорировать его, если этот вариант не относится к вам. Вариант {timeout: 0} — это основное предложение, которое я предлагаю здесь на данный момент. Любопытно услышать, как у вас дела, и рад видеть ссылку на проблему, если вы ее откроете.

ggorlen 31.01.2023 18:48

Я посмотрел на это немного больше. Переход на 19.6.0 устраняет проблему для меня, так что это моя текущая рекомендация. Я открыла выпуск GH.

ggorlen 01.02.2023 04:07

Спасибо. Это будет моим планом действий на данный момент. Я приму этот ответ, так как решения нет, и нам нужно выпустить исправление ошибки.

Matt Welke 01.02.2023 04:19

Звучит хорошо, спасибо за супер ясный вопрос. Интересно, проблема в этом изменении в WaitTask, которое более строго охраняет clearTimeout?

ggorlen 01.02.2023 04:37

Кажется, это все. Я открыл PR, так что это должно быть решено в следующей версии.

ggorlen 01.02.2023 05:20

19.6.3 развернут с исправлением.

ggorlen 01.02.2023 16:41

Другие вопросы по теме