Puppeteer Frame Detachment Problem
I’m working on a Node.js app that automates test solving on a Ukrainian educational platform using puppeteer and puppeteer-cluster. The automation works fine initially but crashes partway through.
My Process
- Log into user account
- Find the specific test
- Open test interface
- Answer questions automatically
The Error
After answering several questions, I get this error: Attempted to use detached Frame 'D78184744F4CA70F50307B66DDF53250'. It happens when trying to find an element with class .quiz-question-header.
async handleQuizQuestion() {
const domHelper = new DomHelper(this.page)
const questionText = await domHelper.extractText('.quiz-question-header') // error occurs here
const choices = await domHelper.extractMultipleTexts(
'.quiz-radio-option',
'.quiz-checkbox-option'
)
console.log({ questionText, choices })
const selectedChoice = await findCorrectAnswer(
questionText || '',
choices
)
console.log({ selectedChoice })
const choiceElements = await domHelper.findElements(
'.quiz-radio-option',
'.quiz-checkbox-option'
)
await choiceElements[selectedChoice - 1].click()
const nextButton = await domHelper.findElement('.submit-quiz-btn')
await nextButton.click()
await delay(3000)
}
import { cleanText, delay } from '@/helpers'
import type { ElementHandle, Page } from 'puppeteer'
export class DomHelper {
constructor(private readonly page: Page) {}
async expandAllContent() {
const domHelper = new DomHelper(this.page)
const expandBtn = await domHelper.findElement('.expand-more-btn')
if (expandBtn) {
await expandBtn.click()
await delay(3000)
await this.expandAllContent()
}
}
async findUniqueElements(selector: string, backup?: string) {
const elements = await this.findElements(selector, backup)
const uniqueElements = (
await Promise.all(elements.map(this.checkSingleClass))
).filter((element) => element !== null)
return uniqueElements
}
async extractUniqueTexts(selector: string, backup?: string) {
const elements = await this.findUniqueElements(selector, backup)
const texts = await Promise.all(
elements.map((element) => element.evaluate((el) => el.textContent))
)
return this.sanitizeTexts(texts)
}
async extractMultipleTexts(selector: string, backup?: string) {
const elements = await this.findElements(selector, backup)
const texts = await Promise.all(
elements.map((element) => element.evaluate((el) => el.textContent))
)
return this.sanitizeTexts(texts)
}
async findElements(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((el) => el.textContent)
const cleanedText = cleanText(text || '')
return cleanedText
}
async findElement(selector: string, backup?: string) {
const elements = await this.findElements(selector, backup)
return elements[0]
}
private async waitAndFindElements(selector: string) {
const el = await this.page
.waitForSelector(selector, {
visible: true,
timeout: 4000,
})
.catch(() => null)
const elText = await el?.evaluate((el) => el.textContent)
console.log({ elText })
return await this.page.$$(selector) // frame detachment error happens here
}
private async checkSingleClass(element: ElementHandle) {
const classProperty = await element.getProperty('className')
const classList = (await classProperty.jsonValue()).toString().split(' ')
return classList.length > 1 ? null : element
}
private async sanitizeTexts(texts: (string | null)[]) {
return texts.map((text) => cleanText(text || ''))
}
}
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',
],
},
})
async runTestAutomation(testName: string) {
await this.loginToAccount()
const quiz = new Quiz(this.page)
const domHelper = new DomHelper(this.page)
await quiz.beginTestByName(testName)
const question = await domHelper.extractText('.quiz-question-header')
console.log({ currentQuestion: question })
while (question) {
await quiz.handleQuizQuestion()
}
}
I’ve tried different puppeteer settings, running in containers, switching browsers, adding navigation waits, and network idle waits. Some approaches just change the error type but don’t fix it. The page URL stays the same when answering questions.