Last active
June 25, 2025 19:24
-
-
Save p32929/dbb594f4cc8a3cdc1cc8a0b9dfbf7918 to your computer and use it in GitHub Desktop.
PuppeteerHelper.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// v4 | |
import { connect } from "puppeteer-real-browser"; | |
import fs from 'fs'; | |
import path from 'path'; | |
// Use the exact types from puppeteer-real-browser | |
type ConnectResult = Awaited<ReturnType<typeof connect>>; | |
type PuppeteerBrowser = ConnectResult['browser']; | |
type PuppeteerPage = ConnectResult['page']; | |
class PuppeteerConstants { | |
static defaultTimeout = 1000 * 60 * 5; | |
static defaultMaxWait = 1000 * 5; | |
static defaultMinWait = 1000; | |
static defaultShortWait = 2500; | |
static defaultDownloadWait = 1000 * 10; | |
static defaultButtonClickTimeout = 1000 * 15; | |
static defaultUploadWait = 1000 * 30; | |
static maxGotoRetries = 5; | |
} | |
interface PuppeteerHelperOptions { | |
headless?: boolean; | |
turnstile?: boolean; | |
args?: string[]; | |
customConfig?: { userDataDir?: string; chromePath?: string; skipDefaultArgs?: boolean; [key: string]: any }; | |
connectOption?: { defaultViewport?: any; args?: string[]; [key: string]: any }; | |
disableXvfb?: boolean; | |
ignoreAllFlags?: boolean; | |
proxy?: { host: string; port: number; username?: string; password?: string }; | |
plugins?: any[]; | |
executablePath?: string; | |
} | |
export class PuppeteerHelper { | |
private browser: PuppeteerBrowser | null = null; | |
private page: PuppeteerPage | null = null; | |
private isInitialized = false; | |
private sessionPath?: string; | |
constructor(private options: PuppeteerHelperOptions = { | |
headless: false, | |
turnstile: true, | |
args: [ | |
"--start-maximized" | |
], | |
customConfig: {}, | |
connectOption: { defaultViewport: null }, | |
disableXvfb: false, | |
ignoreAllFlags: false, | |
plugins: [require("puppeteer-extra-plugin-click-and-wait")()] | |
}, sessionPath?: string) { | |
this.sessionPath = sessionPath; | |
} | |
// Enhanced session management (cookies + localStorage + sessionStorage) | |
private async saveSession() { | |
if (!this.sessionPath || !this.page) return; | |
try { | |
// Save cookies | |
const cookies = await this.page.cookies(); | |
// Save localStorage and sessionStorage | |
const storage = await this.page.evaluate(() => { | |
const localStorage = Object.keys(window.localStorage || {}).reduce((acc, key) => { | |
acc[key] = window.localStorage.getItem(key); | |
return acc; | |
}, {} as Record<string, string | null>); | |
const sessionStorage = Object.keys(window.sessionStorage || {}).reduce((acc, key) => { | |
acc[key] = window.sessionStorage.getItem(key); | |
return acc; | |
}, {} as Record<string, string | null>); | |
return { localStorage, sessionStorage }; | |
}); | |
const sessionData = { | |
cookies, | |
storage, | |
timestamp: Date.now(), | |
url: await this.page.url() | |
}; | |
const sessionFile = path.join(this.sessionPath, 'session-data.json'); | |
fs.mkdirSync(this.sessionPath, { recursive: true }); | |
fs.writeFileSync(sessionFile, JSON.stringify(sessionData, null, 2)); | |
console.log(`πΎ Session saved: ${cookies.length} cookies + storage data`); | |
} catch (error) { | |
console.warn("β οΈ Could not save session:", error); | |
} | |
} | |
private async loadSession() { | |
if (!this.sessionPath || !this.page) return; | |
try { | |
const sessionFile = path.join(this.sessionPath, 'session-data.json'); | |
if (fs.existsSync(sessionFile)) { | |
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); | |
// Check session expiration | |
const sessionMaxAge = (this as any).sessionMaxAge || (72 * 60 * 60 * 1000); // Default 72 hours | |
const sessionAge = Date.now() - (sessionData.timestamp || 0); | |
if (sessionAge > sessionMaxAge) { | |
console.log(`β° Session expired (${Math.round(sessionAge / (1000 * 60 * 60))} hours old), starting fresh`); | |
await PuppeteerHelper.deleteSession(path.basename(this.sessionPath)); | |
return; | |
} | |
// Load cookies first | |
if (sessionData.cookies && sessionData.cookies.length > 0) { | |
await this.page.setCookie(...sessionData.cookies); | |
} | |
// Store storage data for later loading (after navigation) | |
(this as any).pendingStorage = sessionData.storage; | |
console.log(`π Session loaded: ${sessionData.cookies?.length || 0} cookies ready + storage pending`); | |
// Log session age | |
if (sessionData.timestamp) { | |
const ageHours = Math.round((Date.now() - sessionData.timestamp) / (1000 * 60 * 60)); | |
console.log(`π Session age: ${ageHours} hours`); | |
} | |
} else { | |
console.log("π No existing session found, starting fresh"); | |
} | |
} catch (error) { | |
console.warn("β οΈ Could not load session:", error); | |
} | |
} | |
// New method to restore storage after navigation | |
private async restoreStorageAfterNavigation() { | |
const pendingStorage = (this as any).pendingStorage; | |
if (!pendingStorage || !this.page) return; | |
try { | |
await this.page.evaluate((storage: { | |
localStorage?: Record<string, string | null>; | |
sessionStorage?: Record<string, string | null>; | |
}) => { | |
try { | |
// Restore localStorage | |
if (storage.localStorage && window.localStorage) { | |
Object.entries(storage.localStorage).forEach(([key, value]) => { | |
if (value !== null && typeof value === 'string') { | |
window.localStorage.setItem(key, value); | |
} | |
}); | |
} | |
// Restore sessionStorage | |
if (storage.sessionStorage && window.sessionStorage) { | |
Object.entries(storage.sessionStorage).forEach(([key, value]) => { | |
if (value !== null && typeof value === 'string') { | |
window.sessionStorage.setItem(key, value); | |
} | |
}); | |
} | |
} catch (e) { | |
// Silently fail if storage is not accessible | |
} | |
}, pendingStorage); | |
console.log(`π Storage restored after navigation`); | |
// Clear pending storage | |
delete (this as any).pendingStorage; | |
} catch (error) { | |
console.warn("β οΈ Could not restore storage after navigation:", error); | |
} | |
} | |
// Enhanced configuration builder with all features | |
private static buildOptions(options?: { | |
headless?: boolean; | |
proxy?: { host: string; port: string; username?: string; password?: string }; | |
userAgent?: string; | |
plugins?: any[]; | |
windowSize?: { width: number; height: number }; | |
executablePath?: string; | |
}): PuppeteerHelperOptions { | |
const args = ["--start-maximized"]; | |
// Add proxy args if provided | |
if (options?.proxy) { | |
args.push(`--proxy-server=${options.proxy.host}:${options.proxy.port}`); | |
} | |
// Add user agent if provided | |
if (options?.userAgent) { | |
args.push(`--user-agent=${options.userAgent}`); | |
} | |
// Add window size if provided | |
if (options?.windowSize) { | |
args.push(`--window-size=${options.windowSize.width},${options.windowSize.height}`); | |
} | |
const baseOptions: PuppeteerHelperOptions = { | |
headless: options?.headless ?? false, | |
turnstile: true, | |
args, | |
customConfig: {}, // Deliberately NOT setting userDataDir to avoid ECONNREFUSED | |
connectOption: { | |
defaultViewport: options?.windowSize ? { | |
width: options.windowSize.width, | |
height: options.windowSize.height | |
} : null, | |
}, | |
disableXvfb: false, | |
ignoreAllFlags: false, | |
plugins: options?.plugins ?? [require("puppeteer-extra-plugin-click-and-wait")()], | |
executablePath: options?.executablePath | |
}; | |
// Add proxy configuration | |
if (options?.proxy) { | |
baseOptions.proxy = { | |
host: options.proxy.host, | |
port: parseInt(options.proxy.port), | |
username: options.proxy.username, | |
password: options.proxy.password | |
}; | |
} | |
return baseOptions; | |
} | |
// Session management methods | |
static createWithSession(sessionName: string, options?: { | |
headless?: boolean; | |
proxy?: { host: string; port: string; username?: string; password?: string }; | |
userAgent?: string; | |
plugins?: any[]; | |
windowSize?: { width: number; height: number }; | |
executablePath?: string; | |
sessionMaxAge?: number; // Session expiration in hours (default: 72 hours) | |
}) { | |
const sessionPath = `./data/sessions/${sessionName}`; | |
// Auto-create the sessions directory structure | |
try { | |
fs.mkdirSync(sessionPath, { recursive: true }); | |
console.log(`π Session directory created: ${sessionPath}`); | |
} catch (error) { | |
// Directory might already exist, which is fine | |
} | |
const sessionOptions = PuppeteerHelper.buildOptions(options); | |
const helper = new PuppeteerHelper(sessionOptions, sessionPath); | |
// Set session max age (default 72 hours) | |
(helper as any).sessionMaxAge = (options?.sessionMaxAge || 72) * 60 * 60 * 1000; | |
return helper; | |
} | |
// Create a temporary instance without session persistence | |
static createTemporary(options?: { | |
headless?: boolean; | |
proxy?: { host: string; port: string; username?: string; password?: string }; | |
userAgent?: string; | |
plugins?: any[]; | |
windowSize?: { width: number; height: number }; | |
executablePath?: string; | |
}) { | |
console.log(`π Creating temporary browser instance (no session persistence)`); | |
const tempOptions = PuppeteerHelper.buildOptions(options); | |
const helper = new PuppeteerHelper(tempOptions); | |
return helper; | |
} | |
async init(retries = 3) { | |
if (this.isInitialized) return { browser: this.browser, page: this.page }; | |
console.log("π Initializing puppeteer-real-browser..."); | |
for (let attempt = 1; attempt <= retries; attempt++) { | |
try { | |
const { browser, page } = await connect(this.options); | |
this.browser = browser; | |
this.page = page; | |
this.isInitialized = true; | |
// Load session if this is a session browser | |
if (this.sessionPath) { | |
await this.loadSession(); | |
} | |
console.log("β Browser initialized successfully!"); | |
return { browser, page }; | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Browser initialization attempt ${attempt}/${retries} failed: ${errorMsg}`); | |
if (attempt === retries) { | |
throw new Error(`Failed to initialize browser after ${retries} attempts: ${errorMsg}`); | |
} | |
// Wait before retry | |
console.log(`β³ Waiting 2 seconds before retry...`); | |
await PuppeteerHelper.delay(2000); | |
} | |
} | |
throw new Error("Unexpected error in browser initialization"); | |
} | |
private async ensureInitialized() { | |
if (!this.isInitialized) { | |
await this.init(); | |
} | |
} | |
async getPage(): Promise<PuppeteerPage> { | |
await this.ensureInitialized(); | |
if (!this.page) { | |
throw new Error("Failed to initialize page"); | |
} | |
return this.page; | |
} | |
async getBrowser() { | |
await this.ensureInitialized(); | |
return this.browser; | |
} | |
async newPage() { | |
const browser = await this.getBrowser(); | |
return await browser?.newPage(); | |
} | |
async destroy() { | |
try { | |
// Save session if this is a session browser | |
if (this.sessionPath) { | |
await this.saveSession(); | |
} | |
if (this.browser) { | |
await this.browser.close(); | |
console.log("π Browser closed successfully!"); | |
} | |
} catch (e) { | |
console.error("Error closing browser:", e); | |
} | |
this.isInitialized = false; | |
this.browser = null; | |
this.page = null; | |
} | |
// Static helper methods that work with any page | |
static getRandomInt(min = 0, max = Number.MAX_VALUE) { | |
return Math.floor(Math.random() * (max - min + 1) + min); | |
} | |
static delay(ms: number) { | |
return new Promise(resolve => setTimeout(resolve, ms)); | |
} | |
static async wait(page: PuppeteerPage, milliseconds?: number) { | |
const timeout = milliseconds ?? PuppeteerHelper.getRandomInt( | |
PuppeteerConstants.defaultMinWait, | |
PuppeteerConstants.defaultMaxWait | |
); | |
console.log(`β³ Waiting ${timeout}ms...`); | |
await PuppeteerHelper.delay(timeout); | |
} | |
static async navigateTo(page: PuppeteerPage, url: string, retries = PuppeteerConstants.maxGotoRetries) { | |
console.log(`π Navigating to: ${url}`); | |
for (let i = 0; i < retries; i++) { | |
try { | |
const response = await page.goto(url, { | |
waitUntil: 'domcontentloaded', | |
timeout: 90000 | |
}); | |
if (response && response.ok()) { | |
console.log(`β Successfully loaded: ${url}`); | |
await PuppeteerHelper.wait(page); | |
return true; | |
} | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Attempt ${i + 1} failed: ${errorMsg}`); | |
if (i < retries - 1) { | |
await PuppeteerHelper.wait(page); | |
} | |
} | |
} | |
console.log(`β Failed to load ${url} after ${retries} attempts`); | |
return false; | |
} | |
static async clickElement(page: PuppeteerPage, selector: string, retries = 3) { | |
console.log(`π±οΈ Clicking: ${selector}`); | |
for (let i = 0; i < retries; i++) { | |
try { | |
await page.waitForSelector(selector, { timeout: PuppeteerConstants.defaultButtonClickTimeout }); | |
await page.click(selector); | |
await PuppeteerHelper.wait(page); | |
console.log(`β Successfully clicked: ${selector}`); | |
return true; | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Click attempt ${i + 1} failed: ${errorMsg}`); | |
if (i < retries - 1) { | |
await PuppeteerHelper.wait(page, 1000); | |
} | |
} | |
} | |
console.log(`β Failed to click ${selector} after ${retries} attempts`); | |
return false; | |
} | |
static async clickAndWaitForNavigation(page: any, selector: string, options?: { timeout?: number }) { | |
console.log(`π±οΈ Click and wait for navigation: ${selector}`); | |
try { | |
if (page.clickAndWaitForNavigation) { | |
// Use the plugin method if available | |
await page.clickAndWaitForNavigation(selector, options); | |
console.log(`β Successfully clicked and waited for navigation: ${selector}`); | |
return true; | |
} else { | |
// Fallback to manual implementation | |
const [response] = await Promise.all([ | |
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: options?.timeout || 30000 }), | |
page.click(selector) | |
]); | |
console.log(`β Successfully clicked and waited for navigation (fallback): ${selector}`); | |
return true; | |
} | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Failed to click and wait for navigation: ${errorMsg}`); | |
return false; | |
} | |
} | |
static async typeText(page: PuppeteerPage, selector: string, text: string, clearFirst = true) { | |
console.log(`β¨οΈ Typing: ${text}`); | |
try { | |
await page.waitForSelector(selector, { timeout: PuppeteerConstants.defaultButtonClickTimeout }); | |
await page.click(selector); | |
if (clearFirst) { | |
await page.evaluate((sel: string) => { | |
const element = document.querySelector(sel) as HTMLInputElement; | |
if (element) { | |
element.value = ''; | |
element.dispatchEvent(new Event('input', { bubbles: true })); | |
} | |
}, selector); | |
} | |
await page.type(selector, text, { delay: 50 }); | |
await PuppeteerHelper.wait(page, 500); | |
console.log(`β Successfully typed text`); | |
return true; | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Failed to type text: ${errorMsg}`); | |
return false; | |
} | |
} | |
static async scroll(page: PuppeteerPage, direction: 'up' | 'down' = 'down', times = 1) { | |
console.log(`π Scrolling ${direction} ${times} times...`); | |
for (let i = 0; i < times; i++) { | |
await page.evaluate((dir) => { | |
if (dir === 'up') { | |
window.scrollBy({ top: -500, behavior: 'smooth' }); | |
} else { | |
window.scrollBy({ top: 500, behavior: 'smooth' }); | |
} | |
}, direction); | |
await PuppeteerHelper.delay(800); | |
} | |
console.log(`β Finished scrolling ${direction}`); | |
} | |
static async scrollToTop(page: PuppeteerPage) { | |
console.log(`π Scrolling to top...`); | |
await page.evaluate(() => { | |
window.scrollTo({ top: 0, behavior: 'smooth' }); | |
}); | |
await PuppeteerHelper.wait(page); | |
} | |
static async scrollToBottom(page: PuppeteerPage) { | |
console.log(`π Scrolling to bottom...`); | |
await page.evaluate(() => { | |
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); | |
}); | |
await PuppeteerHelper.wait(page); | |
} | |
static async getCurrentUrl(page: PuppeteerPage) { | |
const url = await page.url(); | |
console.log(`π Current URL: ${url}`); | |
return url; | |
} | |
static async takeScreenshot(page: PuppeteerPage, filepath = `./screenshots/screenshot-${Date.now()}.png`) { | |
try { | |
// Ensure directory exists | |
const dir = path.dirname(filepath); | |
await fs.promises.mkdir(dir, { recursive: true }); | |
await page.screenshot({ path: filepath, fullPage: true }); | |
console.log(`πΈ Screenshot saved: ${filepath}`); | |
return filepath; | |
} catch (e: any) { | |
console.log(`β Failed to take screenshot:`, e?.message || e); | |
return null; | |
} | |
} | |
static async getPageTitle(page: PuppeteerPage) { | |
const title = await page.title(); | |
console.log(`π Page title: ${title}`); | |
return title; | |
} | |
static async waitForElement(page: PuppeteerPage, selector: string, timeout = 30000) { | |
console.log(`β³ Waiting for element: ${selector}`); | |
try { | |
await page.waitForSelector(selector, { timeout }); | |
console.log(`β Element found: ${selector}`); | |
return true; | |
} catch (e) { | |
console.log(`β Element not found: ${selector}`); | |
return false; | |
} | |
} | |
static async isElementVisible(page: PuppeteerPage, selector: string) { | |
try { | |
const element = await page.$(selector); | |
if (!element) return false; | |
const isVisible = await page.evaluate((el: any) => { | |
const style = window.getComputedStyle(el); | |
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; | |
}, element); | |
return isVisible; | |
} catch (e) { | |
return false; | |
} | |
} | |
static async getText(page: any, selector: string) { | |
try { | |
const text = await page.$eval(selector, (el: any) => el.textContent?.trim()); | |
console.log(`π Text from ${selector}: ${text}`); | |
return text; | |
} catch (e) { | |
console.log(`β Failed to get text from ${selector}`); | |
return null; | |
} | |
} | |
static async getAllTexts(page: any, selector: string) { | |
try { | |
const texts = await page.$$eval(selector, (elements: any[]) => | |
elements.map((el: any) => el.textContent?.trim()).filter((text: any) => text) | |
); | |
console.log(`π Found ${texts.length} text elements for ${selector}`); | |
return texts; | |
} catch (e) { | |
console.log(`β Failed to get texts from ${selector}`); | |
return []; | |
} | |
} | |
static async uploadFile(page: any, inputSelector: string, filePath: string) { | |
console.log(`π€ Uploading file: ${filePath} to ${inputSelector}`); | |
try { | |
const input = await page.$(inputSelector); | |
if (!input) { | |
console.log(`β File input not found: ${inputSelector}`); | |
return false; | |
} | |
await input.uploadFile(filePath); | |
await PuppeteerHelper.wait(page, PuppeteerConstants.defaultUploadWait); | |
console.log(`β File uploaded successfully`); | |
return true; | |
} catch (e: any) { | |
console.log(`β Failed to upload file:`, e?.message || e); | |
return false; | |
} | |
} | |
static async downloadFile(page: any, url: string, downloadPath: string) { | |
console.log(`β¬οΈ Downloading file from: ${url}`); | |
try { | |
const response = await page.goto(url); | |
const buffer = await response.buffer(); | |
// Ensure directory exists | |
const dir = path.dirname(downloadPath); | |
await fs.promises.mkdir(dir, { recursive: true }); | |
await fs.promises.writeFile(downloadPath, buffer); | |
console.log(`β File downloaded: ${downloadPath}`); | |
return true; | |
} catch (e: any) { | |
console.log(`β Failed to download file:`, e?.message || e); | |
return false; | |
} | |
} | |
static async setViewport(page: any, width: number, height: number) { | |
console.log(`π± Setting viewport: ${width}x${height}`); | |
try { | |
await page.setViewport({ width, height }); | |
return true; | |
} catch (e: any) { | |
console.log(`β Failed to set viewport:`, e?.message || e); | |
return false; | |
} | |
} | |
static async setMobileViewport(page: any) { | |
return await PuppeteerHelper.setViewport(page, 390, 844); | |
} | |
static async setDesktopViewport(page: any) { | |
return await PuppeteerHelper.setViewport(page, 1920, 1080); | |
} | |
static async evaluateScript(page: any, script: string, ...args: any[]) { | |
try { | |
const result = await page.evaluate(script, ...args); | |
console.log(`β Script executed successfully`); | |
return result; | |
} catch (e: any) { | |
console.log(`β Script execution failed:`, e?.message || e); | |
return null; | |
} | |
} | |
static async addStylesheet(page: any, css: string) { | |
try { | |
await page.addStyleTag({ content: css }); | |
console.log(`β CSS added successfully`); | |
return true; | |
} catch (e: any) { | |
console.log(`β Failed to add CSS:`, e?.message || e); | |
return false; | |
} | |
} | |
static async blockResources(page: PuppeteerPage, resourceTypes = ['image', 'media', 'font']) { | |
console.log(`π« Blocking resources: ${resourceTypes.join(', ')}`); | |
await page.setRequestInterception(true); | |
page.on('request', (req: any) => { | |
if (resourceTypes.includes(req.resourceType())) { | |
req.abort(); | |
} else { | |
req.continue(); | |
} | |
}); | |
} | |
// Simple utility functions | |
static async clearInput(page: PuppeteerPage, selector: string) { | |
console.log(`π§Ή Clearing input`); | |
try { | |
await page.click(selector, { clickCount: 3 }); | |
await page.keyboard.press('Backspace'); | |
console.log(`β Input cleared`); | |
return true; | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Failed to clear input: ${errorMsg}`); | |
return false; | |
} | |
} | |
static async selectDropdownOption(page: PuppeteerPage, selector: string, value: string) { | |
console.log(`π― Selecting option: ${value}`); | |
try { | |
await page.select(selector, value); | |
console.log(`β Option selected`); | |
return true; | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Failed to select option: ${errorMsg}`); | |
return false; | |
} | |
} | |
static async hoverElement(page: PuppeteerPage, selector: string) { | |
console.log(`π±οΈ Hovering over element`); | |
try { | |
await page.hover(selector); | |
await PuppeteerHelper.wait(page, 500); | |
console.log(`β Hovered successfully`); | |
return true; | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Failed to hover: ${errorMsg}`); | |
return false; | |
} | |
} | |
static async refresh(page: PuppeteerPage) { | |
console.log(`π Refreshing page`); | |
try { | |
await page.reload({ waitUntil: 'domcontentloaded' }); | |
await PuppeteerHelper.wait(page); | |
console.log(`β Page refreshed`); | |
return true; | |
} catch (e: any) { | |
console.log(`β Failed to refresh`); | |
return false; | |
} | |
} | |
static async goBack(page: PuppeteerPage) { | |
console.log(`β¬ οΈ Going back`); | |
try { | |
await page.goBack({ waitUntil: 'domcontentloaded' }); | |
await PuppeteerHelper.wait(page); | |
console.log(`β Went back successfully`); | |
return true; | |
} catch (e: any) { | |
console.log(`β Failed to go back`); | |
return false; | |
} | |
} | |
static async pressKey(page: PuppeteerPage, key: string) { | |
console.log(`β¨οΈ Pressing: ${key}`); | |
try { | |
await page.keyboard.press(key as any); | |
await PuppeteerHelper.wait(page, 200); | |
console.log(`β Key pressed`); | |
return true; | |
} catch (e: any) { | |
console.log(`β Failed to press key`); | |
return false; | |
} | |
} | |
static async waitForText(page: PuppeteerPage, text: string, timeout = 10000) { | |
console.log(`β³ Waiting for text: "${text}"`); | |
try { | |
await page.waitForFunction( | |
(txt) => document.body.innerText.includes(txt), | |
{ timeout }, | |
text | |
); | |
console.log(`β Text found`); | |
return true; | |
} catch (e: any) { | |
console.log(`β Text not found`); | |
return false; | |
} | |
} | |
static async getElementCount(page: PuppeteerPage, selector: string) { | |
try { | |
const elements = await page.$$(selector); | |
console.log(`π’ Found ${elements.length} elements`); | |
return elements.length; | |
} catch (e: any) { | |
console.log(`β Failed to count elements`); | |
return 0; | |
} | |
} | |
// Session utility methods | |
static async listSessions() { | |
try { | |
const sessionsDir = './data/sessions'; | |
if (!fs.existsSync(sessionsDir)) { | |
console.log(`π No sessions directory found`); | |
return []; | |
} | |
const sessions = fs.readdirSync(sessionsDir, { withFileTypes: true }) | |
.filter(dirent => dirent.isDirectory()) | |
.map(dirent => dirent.name); | |
console.log(`π Found ${sessions.length} sessions: ${sessions.join(', ')}`); | |
return sessions; | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Failed to list sessions: ${errorMsg}`); | |
return []; | |
} | |
} | |
static async deleteSession(sessionName: string) { | |
try { | |
const sessionPath = `./data/sessions/${sessionName}`; | |
if (fs.existsSync(sessionPath)) { | |
await fs.promises.rm(sessionPath, { recursive: true, force: true }); | |
console.log(`ποΈ Session deleted: ${sessionName}`); | |
return true; | |
} else { | |
console.log(`β οΈ Session not found: ${sessionName}`); | |
return false; | |
} | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Failed to delete session: ${errorMsg}`); | |
return false; | |
} | |
} | |
static async sessionExists(sessionName: string) { | |
const sessionPath = `./data/sessions/${sessionName}`; | |
return fs.existsSync(sessionPath); | |
} | |
// Get current session name (if any) - improved implementation | |
getSessionName() { | |
if (this.sessionPath) { | |
return path.basename(this.sessionPath); | |
} | |
return null; | |
} | |
// Instance methods for convenience (enhanced with storage restoration) | |
async navigate(url: string) { | |
const page = await this.getPage(); | |
if (!page) throw new Error("Page not initialized"); | |
const result = await PuppeteerHelper.navigateTo(page, url); | |
// Restore storage after navigation if we have pending storage | |
await this.restoreStorageAfterNavigation(); | |
return result; | |
} | |
async click(selector: string, retries = 3) { | |
const page = await this.getPage(); | |
return await PuppeteerHelper.clickElement(page, selector, retries); | |
} | |
async type(selector: string, text: string, clearFirst = true) { | |
const page = await this.getPage(); | |
return await PuppeteerHelper.typeText(page, selector, text, clearFirst); | |
} | |
async wait(milliseconds?: number) { | |
const page = await this.getPage(); | |
return await PuppeteerHelper.wait(page, milliseconds); | |
} | |
async screenshot(filepath?: string) { | |
const page = await this.getPage(); | |
return await PuppeteerHelper.takeScreenshot(page, filepath); | |
} | |
async scroll(direction: 'up' | 'down' = 'down', times = 1) { | |
const page = await this.getPage(); | |
return await PuppeteerHelper.scroll(page, direction, times); | |
} | |
async getNewPage(): Promise<PuppeteerPage> { | |
await this.ensureInitialized(); | |
if (!this.browser) { | |
throw new Error("Browser not initialized"); | |
} | |
console.log('π Creating new page...'); | |
const newPage = await this.browser.newPage(); | |
console.log('β New page created successfully'); | |
return newPage as PuppeteerPage; | |
} | |
static async cleanupExpiredSessions(maxAgeHours = 72) { | |
try { | |
const sessionsDir = './data/sessions'; | |
if (!fs.existsSync(sessionsDir)) { | |
return []; | |
} | |
const sessions = fs.readdirSync(sessionsDir, { withFileTypes: true }) | |
.filter(dirent => dirent.isDirectory()) | |
.map(dirent => dirent.name); | |
const maxAge = maxAgeHours * 60 * 60 * 1000; | |
const cleanedSessions: string[] = []; | |
for (const sessionName of sessions) { | |
const sessionFile = path.join(sessionsDir, sessionName, 'session-data.json'); | |
if (fs.existsSync(sessionFile)) { | |
try { | |
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); | |
const sessionAge = Date.now() - (sessionData.timestamp || 0); | |
if (sessionAge > maxAge) { | |
await PuppeteerHelper.deleteSession(sessionName); | |
cleanedSessions.push(sessionName); | |
} | |
} catch (error) { | |
// If session file is corrupted, delete it | |
console.log(`ποΈ Removing corrupted session: ${sessionName}`); | |
await PuppeteerHelper.deleteSession(sessionName); | |
cleanedSessions.push(sessionName); | |
} | |
} | |
} | |
if (cleanedSessions.length > 0) { | |
console.log(`π§Ή Cleaned up ${cleanedSessions.length} expired sessions: ${cleanedSessions.join(', ')}`); | |
} | |
return cleanedSessions; | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Failed to cleanup sessions: ${errorMsg}`); | |
return []; | |
} | |
} | |
static async getSessionInfo(sessionName: string) { | |
try { | |
const sessionPath = `./data/sessions/${sessionName}`; | |
const sessionFile = path.join(sessionPath, 'session-data.json'); | |
if (!fs.existsSync(sessionFile)) { | |
return null; | |
} | |
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); | |
const sessionAge = Date.now() - (sessionData.timestamp || 0); | |
const ageHours = Math.round(sessionAge / (1000 * 60 * 60)); | |
return { | |
name: sessionName, | |
ageHours, | |
cookieCount: sessionData.cookies?.length || 0, | |
hasStorage: !!(sessionData.storage?.localStorage || sessionData.storage?.sessionStorage), | |
lastUrl: sessionData.url, | |
created: new Date(sessionData.timestamp || 0).toISOString() | |
}; | |
} catch (error) { | |
return null; | |
} | |
} | |
// Enhanced session listing with detailed info | |
static async listSessionsDetailed() { | |
try { | |
const sessions = await PuppeteerHelper.listSessions(); | |
const sessionDetails = await Promise.all( | |
sessions.map(sessionName => PuppeteerHelper.getSessionInfo(sessionName)) | |
); | |
return sessionDetails.filter(Boolean); | |
} catch (error) { | |
const errorMsg = error instanceof Error ? error.message : String(error); | |
console.log(`β Failed to list sessions: ${errorMsg}`); | |
return []; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment