Ошибка драматурга (цель закрыта) после навигации
Я пробую что-то очень простое:
- Перейдите на google.com
- Заполните поле поиска словом "сыр"
- Нажмите Enter в поле поиска
- Выведите текст заголовка первого результата
Так просто, но я не могу заставить его работать. Это код:
const playwright = require('playwright');
(async () => {
for (const browserType of ['chromium', 'firefox', 'webkit']) {
const browser = await playwright[browserType].launch();
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://google.com');
await page.fill('input[name=q]', 'cheese');
await page.press('input[name=q]', 'Enter');
await page.waitForNavigation();
page.waitForSelector('div#rso h3')
.then(firstResult => console.log(`${browserType}: ${firstResult.textContent()}`))
.catch(error => console.error(`Waiting for result: ${error}`));
} catch(error) {
console.error(`Trying to run test on ${browserType}: ${error}`);
} finally {
await browser.close();
}
}
})();
Сначала я пытался получить первый результат с помощью page.$()
но это не сработало. Немного изучив проблему, я обнаружил, чтоpage.waitForNavigation()
я думал, что это решение, но это не так.
Я использую последнюю версию драматурга: 1.0.2.
4 ответа
Мне кажется, единственная проблема заключалась в том, что вы оценили textContent
неправильно. Реорганизовавpage.waitForSelector
обещать async / await и использовать page.$eval
получить textContent
он работает отлично, больше нет целевых закрытых ошибок (например, вы случайно захотели использоватьwaitForSelector
оценить ElementHandle?).
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://google.com');
await page.fill('input[name=q]', 'cheese');
await page.press('input[name=q]', 'Enter');
await page.waitForNavigation();
// page.waitForSelector('div#rso h3').then(firstResult => console.log(`${browserType}: ${firstResult.textContent()}`)).catch(error => console.error(`Waiting for result: ${error}`));
await page.waitForSelector('div#rso h3');
const firstResult = await page.$eval('div#rso h3', firstRes => firstRes.textContent);
console.log(`${browserType}: ${firstResult}`)
} catch(error) {
console.error(`Trying to run test on ${browserType}: ${error}`);
} finally {
await browser.close();
}
}
Выход:
chrome: Cheese – Wikipedia
firefox: Cheese – Wikipedia
webkit: Cheese – Wikipedia
Примечание: Chrome и webkit работают, firefox не работает.waitForNavigation
для меня. Если бы я заменил его наawait page.waitForTimeout(5000);
firefox тоже работал. Это может быть проблема с поддержкой Firefox драматургом для обещания навигации.
В моем случае ошибка драматурга
Target closed
появляется при первой попытке получить текст со страницы. Ошибка неверна, фактическая причина заключалась в том, что на целевом сайте была включена базовая аутентификация. Драматург не смог открыть страницу и просто застрял с надписью «Цель закрыта».const options = { httpCredentials = { username: 'user', password: 'password'} }; const context = await browser.newContext(options);
Еще одна проблема заключалась в том, что локальные тесты запускались без проблем, включая контейнеры Docker, в то время как Github CI терпел неудачу с Playwright без каких-либо подробностей, кроме приведенной выше ошибки.
Причина была в специальном символе в Github Secret. Например, знак доллара будет просто удален из секрета в Github Actions. Чтобы исправить это, используйте либоenv:
разделenv: XXX: ${ secrets.SUPER_SECRET }
или заключить секрет в одинарные кавычки:
run: | export XXX='${{ secrets.YYY}}'
Аналогичная специфика экранирования существует в Kubernetes, Docker и Gitlub;
$$
становится$
а такжеz$abc
становитсяz
.Использовать
mcr.microsoft.com/playwright
образ docker hub от Microsoft с предустановленными node, npm и playwright. В качестве альтернативы во время установки playwright не забудьте установить зависимости системных пакетов, запустивnpx playwright install-deps
.Виртуальная машина должна иметь достаточно ресурсов для обработки тестов браузера. Распространенная проблема в рабочих процессах CI/CD.
Если вы ждете page.press('input[name=q]', 'Enter');
это может быть слишком поздно для waitForNavigation
работать.
Вы можете удалить await
на звонке для прессы. Вам может потребоваться подождать навигации, а не действия нажатия.
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://google.com');
await page.fill('input[name=q]', 'cheese');
page.press('input[name=q]', 'Enter');
await page.waitForNavigation();
var firstResult = await page.waitForSelector('div#rso h3');
console.log(`${browserType}: ${await firstResult.textContent()}`);
Также обратите внимание, что вам нужно await
за textContent()
.
Обязательно выполняйте все обещания и избегайте объединения
then
с
await
.
//vvv
await page.waitForSelector('div#rso h3')
//^^^
Обратите внимание, чтоawait page.waitForNavigation();
может вызвать состояние гонки, если вызывается после события, которое запускает навигацию. Обычно я избегаю ожидания селектора или условия, которое появится на следующей странице. Обычно это приводит к более быстрому, короткому и надежному коду.
Если вы используетеwaitForNavigation
, установите его рядом сPromise.all
или до события триггера навигации, в этом случаеpress
.
После этих корректировок, если ваша цель состоит в том, чтобы получить данные как можно быстрее и надежнее, а не тестировать шаги на этом пути, есть возможности для улучшения.
Часто нет необходимости переходить на целевую страницу, а затем вводить текст в поле, чтобы запустить поиск. Обычно быстрее и менее подвержено ошибкам перейти непосредственно на страницу результатов с запросом, закодированным в URL-адресе. В этом случае ваш код можно сократить до
const url = "https://www.google.com/search?q=cheese";
await page.goto(url, {waitUntil: "networkidle"});
console.log(await page.textContent(".fP1Qef h3"));
Если вы заметили, что нужный вам текст находится в статическом HTML, как в данном случае, вы можете пойти дальше и заблокировать JS и внешние ресурсы:
const playwright = require("playwright"); // ^1.30.1
let browser;
let context;
(async () => {
browser = await playwright.chromium.launch();
context = await browser.newContext({javaScriptEnabled: false});
const page = await context.newPage();
const url = "https://www.google.com/search?q=cheese";
await page.route("**", route => {
if (route.request().url().startsWith(url)) {
route.continue();
}
else {
route.abort();
}
});
// networkidle is a suboptimal way to handle redirection
await page.goto(url, {waitUntil: "networkidle"});
console.log(await page.locator(".fP1Qef h3").allTextContents());
})()
.catch(err => console.error(err))
.finally(async () => {
await context?.close();
await browser?.close();
});
Заблокировав JS и все внешние ресурсы, вы часто можете перейти к Святому Граалю парсинга веб-страниц: полностью отказаться от автоматизации браузера и вместо этого использовать HTTP-запрос и облегченный парсер HTML:
const cheerio = require("cheerio"); // 1.0.0-rc.12
const query = "cheese";
const url = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
fetch(url, { // Node 18 or install node-fetch
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
}
})
.then(res => res.text())
.then(html => {
const $ = cheerio.load(html);
console.log($(".LC20lb").first().text()); // first result
console.log([...$(".LC20lb")].map(e => $(e).text())); // all results
});