Puppeteer Frame Detachment Error During Test Automation
I’m working on a Node.js app that automates a Ukrainian testing website using puppeteer and puppeteer-cluster. The automation process works like this:
- Log into user dashboard
- Find the specific test
- Open the test
- Complete questions automatically
Everything works fine until I’m partway through answering questions. Then I get this error: Attempted to use detached Frame 'D78184744F4CA70F50307B66DDF53250'
The error happens when trying to find an element with class .quiz-question-header. The element definitely exists on the page.
async handleMultipleChoiceQuestion() {
const domHelper = new DomHelper(this.page)
const questionText = await domHelper.extractText('.quiz-question-header') // Error occurs here
const options = await domHelper.extractMultipleTexts(
'.quiz-option-radio',
'.quiz-option-checkbox'
)
console.log({ questionText, options })
const selectedOption = await getAnswerChoice(
questionText || '',
options
)
console.log({ selectedOption })
const optionElements = await domHelper.findMultipleElements(
'.quiz-option-radio',
'.quiz-option-checkbox'
)
await optionElements[selectedOption - 1].click()
const nextButton = await domHelper.findElement('.submit-quiz-btn')
await nextButton.click()
await delay(3000)
}
Here’s my DomHelper class:
import { sanitizeText, delay } from '@/helpers'
import type { ElementHandle, Page } from 'puppeteer'
export class DomHelper {
constructor(private readonly page: Page) {}
async loadMoreContent() {
const domHelper = new DomHelper(this.page)
const loadButton = await domHelper.findElement('.load-more-btn')
if (loadButton) {
await loadButton.click()
await delay(3000)
await this.loadMoreContent()
}
}
async findUniqueElements(selector: string, backup?: string) {
const elements = await this.findMultipleElements(selector, backup)
const uniqueElements = (await Promise.all(elements.map(this.hasSingleClass))).filter((el) => el !== null)
return uniqueElements
}
async extractUniqueTexts(selector: string, backup?: string) {
const elements = await this.findUniqueElements(selector, backup)
const texts = await Promise.all(elements.map((el) => el.evaluate((node) => node.textContent)))
return this.sanitizeTexts(texts)
}
async extractMultipleTexts(selector: string, backup?: string) {
const elements = await this.findMultipleElements(selector, backup)
const texts = await Promise.all(elements.map((el) => el.evaluate((node) => node.textContent)))
return this.sanitizeTexts(texts)
}
async findMultipleElements(selector: string, backup?: string) {
const elements = await this.waitAndFindElements(selector)
if (elements.length > 0) return elements
if (backup) return await this.waitAndFindElements(backup)
return []
}
async extractText(selector: string, backup?: string) {
const element = await this.findElement(selector, backup)
const text = await element?.evaluate((node) => node.textContent)
return sanitizeText(text || '')
}
async findElement(selector: string, backup?: string) {
const elements = await this.findMultipleElements(selector, backup)
return elements[0]
}
private async waitAndFindElements(selector: string) {
const element = await this.page.waitForSelector(selector, {
visible: true,
timeout: 4000,
}).catch(() => null)
const textContent = await element?.evaluate((node) => node.textContent)
console.log({ textContent })
return await this.page.$$(selector) // Error points here
}
private async hasSingleClass(element: ElementHandle) {
const classProperty = await element.getProperty('className')
const classes = (await classProperty.jsonValue()).toString().split(' ')
return classes.length > 1 ? null : element
}
private async sanitizeTexts(texts: (string | null)[]) {
return texts.map((text) => sanitizeText(text || ''))
}
}
Cluster setup:
import { Cluster } from 'puppeteer-cluster'
export const cluster = await Cluster.launch({
concurrency: Cluster.CONCURRENCY_CONTEXT,
maxConcurrency: 500,
puppeteerOptions: {
headless: true,
dumpio: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
},
})
Main service method:
async automateTestCompletion(testName: string) {
await this.loginToAccount()
const testHandler = new TestHandler(this.page)
const domHelper = new DomHelper(this.page)
await testHandler.beginTestByName(testName)
const currentQuestion = await domHelper.extractText('.quiz-question-header')
console.log({ currentQuestionInService: currentQuestion })
while (currentQuestion) {
await testHandler.handleMultipleChoiceQuestion()
}
}
I’ve tried different approaches like modifying browser settings, using Docker containers, switching browsers, adding page.waitForNavigation() and page.waitForNetworkIdle() calls. Nothing fixes it completely, though some changes just produce different error messages.
Note that the page URL stays the same when answering questions.