Last active
September 7, 2024 15:44
-
-
Save p32929/7a2375cf2eb3d2986a741d7dc293a4c8 to your computer and use it in GitHub Desktop.
Playwright utility functions
This file contains 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
// v0.0.7 | |
import { Page, BrowserType, BrowserContext, chromium, firefox } from "playwright"; | |
class ChromeConstants { | |
static SHOULD_CRASH_AFTER_URL_RETRY = true | |
static dbPath = "./data/database.json" | |
static defaultChromeTimeout = 1000 * 60 * 5 | |
static defaultMaxWaitMs = 1000 * 5 | |
static defaultMinWaitMs = 1000 | |
static defaultShortWait = 2000 | |
static defaultDownloadWaitMs = 1000 * 10 | |
static defaultButtonClickTimeout = 1000 * 15 | |
static defaultButtonClickDelay = 500 | |
static defaultUploadWaitMs = 1000 * 30 | |
static maxGotoRetries = 5 | |
} | |
type BrowserTypes = "chrome" | "firefox" | |
interface IBrowserOptions { | |
mode: "sessioned" | "private", | |
sessionPath: string, | |
timeout: number, | |
browser: BrowserTypes, | |
headless: boolean, | |
/* | |
In order to mute browser completely, use this: | |
https://addons.mozilla.org/en-US/firefox/addon/mute-sites-by-default/ | |
https://chrome.google.com/webstore/detail/clever-mute/eadinjjkfelcokdlmoechclnmmmjnpdh | |
*/ | |
/* | |
In order to mute block images/videos/etc completely, use this: | |
https://addons.mozilla.org/en-US/firefox/addon/image-video-block/?utm_source=addons.mozilla.org&utm_medium=referral&utm_content=search | |
*/ | |
} | |
const defaultValues: IBrowserOptions = { | |
mode: "sessioned", | |
sessionPath: `./data/sessions/`, | |
timeout: ChromeConstants.defaultChromeTimeout, | |
browser: "firefox", | |
headless: false, | |
} | |
let openingUrl = "" | |
let originalViewport = null | |
const getRandomInt = (min: number = 0, max: number = Number.MAX_VALUE) => { | |
const int = Math.floor(Math.random() * (max - min + 1) + min) | |
return int | |
} | |
const delay = (ms) => new Promise((res) => setTimeout(res, ms)); | |
// | |
export class Chrome { | |
private options: IBrowserOptions = defaultValues | |
private page: Page = null | |
private context: BrowserContext = null | |
private isInitting = false | |
private openedPages: number = 0 | |
private tryingToOpenPages: number = 0 | |
constructor(options: Partial<IBrowserOptions> = defaultValues) { | |
this.options = { | |
...defaultValues, | |
...options, | |
} | |
} | |
private getBrowser(): BrowserType<{}> { | |
console.log(`chrome.ts :: Chrome :: getBrowser :: `) | |
if (this.options.browser === 'chrome') { | |
return chromium | |
} | |
else if (this.options.browser === 'firefox') { | |
return firefox | |
} | |
} | |
async getNewPage() { | |
console.log(`chrome.ts :: Chrome :: getNewPage :: this.openedPages -> ${this.openedPages} , this.context.pages().length -> ${this?.context?.pages().length} `) | |
while (this.isInitting) { | |
console.log(`chrome.ts :: Chrome :: getNewPage :: this.isInitting -> ${this.isInitting} `) | |
await delay(ChromeConstants.defaultShortWait) | |
} | |
console.log(`chrome.ts :: Chrome :: getNewPage :: this.isInitting -> ${this.isInitting} `) | |
if (this.isInitting === false && this.context === null) { | |
this.isInitting = true | |
if (this.options.mode == "sessioned") { | |
this.context = await this.getBrowser().launchPersistentContext( | |
this.options.sessionPath, { | |
headless: this.options.headless, | |
timeout: this.options.timeout, | |
ignoreHTTPSErrors: true, | |
}) | |
this.context.setDefaultNavigationTimeout(this.options.timeout) | |
this.context.setDefaultTimeout(this.options.timeout) | |
} | |
else if (this.options.mode == "private") { | |
const browser = await this.getBrowser().launch({ | |
headless: this.options.headless, | |
timeout: this.options.timeout, | |
}); | |
this.context = await browser.newContext({ | |
ignoreHTTPSErrors: true, | |
}) | |
this.context.setDefaultNavigationTimeout(this.options.timeout) | |
this.context.setDefaultTimeout(this.options.timeout) | |
} | |
await this.context.addInitScript("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") | |
this.isInitting = false | |
} | |
console.log(`chrome.ts :: Chrome :: getNewPage-1 :: this.tryingToOpenPages -> ${this.tryingToOpenPages} , this.openedPages -> ${this.openedPages} `) | |
while (this.tryingToOpenPages !== this.openedPages) { | |
await delay(ChromeConstants.defaultShortWait) | |
console.log(`chrome.ts :: Chrome :: getNewPage-1 :: this.tryingToOpenPages -> ${this.tryingToOpenPages} , this.openedPages -> ${this.openedPages} `) | |
} | |
this.tryingToOpenPages++ | |
this.page = await this.context.newPage(); | |
this.openedPages++ | |
return this.page | |
} | |
async destroy() { | |
try { | |
const pages = this.context.pages() | |
for (var i = 0; i < pages.length; i++) { | |
await pages[i].close() | |
} | |
await this.context.close() | |
} | |
catch (e) { | |
// | |
} | |
} | |
// ############################# | |
// ############################# | |
// ############################# | |
static async downloadFile(page: Page, url: string, filePath: string, waitTimeout: number = ChromeConstants.defaultDownloadWaitMs): Promise<boolean> { | |
console.log(`chrome.ts :: Chrome :: downloadFile :: url -> ${url} , filePath -> ${filePath} `) | |
return new Promise(async (resolve) => { | |
try { | |
page.evaluate((link) => { | |
function download(url, filename) { | |
fetch(url) | |
.then(response => response.blob()) | |
.then(blob => { | |
const link = document.createElement("a"); | |
link.href = URL.createObjectURL(blob); | |
link.download = filename; | |
link.click(); | |
}) | |
.catch(console.error); | |
} | |
download(link, "somefile.someext") | |
}, url) | |
const [download] = await Promise.all([ | |
page.waitForEvent('download', { timeout: waitTimeout }), | |
]); | |
await download.saveAs(filePath) | |
await Chrome.waitForTimeout(page) | |
resolve(true) | |
} catch (e) { | |
resolve(false) | |
} | |
}) | |
} | |
static async downloadFileByButtonClick(page: Page, buttonSelector: string, filePath: string): Promise<boolean> { | |
console.log(`chrome.ts :: Chrome :: downloadFileByButtonClick :: buttonSelector -> ${buttonSelector} , filePath -> ${filePath} `) | |
return new Promise(async (resolve) => { | |
try { | |
const downloadPromise = page.waitForEvent('download'); | |
await page.click(buttonSelector) | |
const download = await downloadPromise; | |
await download.saveAs(filePath); | |
await Chrome.waitForTimeout(page, { | |
maxTimeout: ChromeConstants.defaultDownloadWaitMs, | |
}) | |
resolve(true) | |
} catch (e) { | |
resolve(false) | |
} | |
}) | |
} | |
static async uploadFiles(page: Page, uploadButtonSelector: string, fileLocations: string | string[], wait: number = ChromeConstants.defaultUploadWaitMs) { | |
console.log(`chrome.ts :: Chrome :: uploadFiles :: uploadButtonSelector -> ${uploadButtonSelector} , fileLocations -> ${fileLocations} `) | |
const [fileChooser] = await Promise.all([ | |
page.waitForEvent('filechooser'), | |
await Chrome.waitForTimeout(page), | |
page.click(uploadButtonSelector), | |
]); | |
await fileChooser.setFiles(fileLocations) | |
await Chrome.waitForTimeout(page, { | |
maxTimeout: ChromeConstants.defaultUploadWaitMs, | |
}) | |
} | |
static async uploadFilesForced(page: Page, uploadButtonSelector: string, fileLocations: string | string[]) { | |
console.log(`chrome.ts :: Chrome :: uploadFiles :: uploadButtonSelector -> ${uploadButtonSelector} , fileLocations -> ${fileLocations} `) | |
const [fileChooser] = await Promise.all([ | |
page.waitForEvent('filechooser'), | |
Chrome.waitForTimeout(page), | |
page.click(uploadButtonSelector), | |
]); | |
// await fileChooser.setFiles(fileLocations) | |
for (var i = 0; i < fileLocations.length; i++) { | |
await fileChooser.setFiles(fileLocations[i]) | |
// await page.waitForTimeout(500) | |
await Chrome.waitForTimeout(page) | |
} | |
await Chrome.waitForTimeout(page, { | |
maxTimeout: ChromeConstants.defaultUploadWaitMs, | |
}) | |
} | |
static async getCurrentHeightWidth(page: Page): Promise<{ | |
height: number; | |
width: number; | |
}> { | |
console.log(`chrome.ts :: Chrome :: getCurrentHeightWidth :: `) | |
const obj = await page.evaluate(() => { | |
return { | |
height: window.outerHeight, | |
width: window.outerWidth, | |
} | |
}) | |
return obj | |
} | |
static async copyTextToClipboard(page: Page, text: string) { | |
console.log(`chrome.ts :: Chrome :: copyTextToClipboard :: text -> ${text} `) | |
await page.evaluate((text) => { | |
navigator.clipboard.writeText(text) | |
}, text) | |
await Chrome.waitForTimeout(page) | |
} | |
static async gotoForce(page: Page, url: string) { | |
const retryCount = ChromeConstants.maxGotoRetries; | |
let openingUrl = ""; // Declare openingUrl here | |
let downloadDetected = false; // Flag to detect if a download was triggered | |
try { | |
const currentLocation = await page.evaluate(() => window.location.href); | |
if (currentLocation === url) { | |
await Chrome.waitForTimeout(page); | |
return; | |
} | |
// Listen for download events and cancel them | |
page.on('download', async (download) => { | |
console.log(`Download detected and canceled: ${download.suggestedFilename()}`); | |
await download.cancel(); // Cancel the download | |
downloadDetected = true; // Set the flag to true | |
}); | |
const tryUrl = async (): Promise<boolean> => { | |
try { | |
const response = await page.goto(url, { | |
timeout: 90 * 1000, | |
waitUntil: 'load', | |
}); | |
const status = response.status(); | |
console.log(`Chrome.ts :: Chrome :: tryUrl :: status -> ${status}`); | |
// Check if the page contains a specific timeout error message | |
if (await page.$('text="The connection has timed out"')) { | |
console.log(`Timeout error detected on the page: ${url}`); | |
return false; | |
} | |
await Chrome.waitForTimeout(page); | |
return true; | |
} catch (e) { | |
console.log(`chrome.ts :: Chrome :: tryUrl :: e -> ${e}`); | |
return false; | |
} | |
}; | |
openingUrl = url; | |
for (let i = 0; i < retryCount; i++) { | |
if (downloadDetected) { | |
console.log(`chrome.ts :: Chrome :: gotoForce= :: Download detected, skipping URL -> ${url}`); | |
break; | |
} | |
const opened = await tryUrl(); | |
console.log(`chrome.ts :: Chrome :: gotoForce= :: url -> ${url} , opened -> ${opened} , i -> ${i}`); | |
if (opened) { | |
openingUrl = ""; | |
break; | |
} else { | |
console.log(`chrome.ts :: Chrome :: gotoForce= :: Retrying... :: url -> ${url} , opened -> ${opened} , i -> ${i}`); | |
await Chrome.waitForTimeout(page); | |
if (i === retryCount - 1) { | |
console.log('Max retries reached. Issue persists.'); | |
} | |
} | |
} | |
if (!downloadDetected) { | |
console.log(`chrome.ts :: Chrome :: gotoForce= :: url -> ${url} , openingUrl -> ${openingUrl} :: Success...`); | |
} | |
openingUrl = ""; | |
} catch (e) { | |
console.log(`chrome.ts :: Chrome :: gotoForce= :: e -> ${e}`); | |
console.log(`chrome.ts :: Chrome :: gotoForce= :: url -> ${url} , openingUrl -> ${openingUrl} :: Failed...`); | |
openingUrl = ""; | |
} | |
}; | |
static async scrollDown(page: Page, nTimes: number = 10, wait: number = ChromeConstants.defaultMaxWaitMs) { | |
console.log(`chrome.ts :: Chrome :: scrollDown :: nTimes -> ${nTimes} , wait -> ${wait} `) | |
for (var i = 0; i < nTimes; i++) { | |
await page.evaluate(() => { | |
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) | |
}) | |
await page.waitForTimeout(wait) | |
} | |
} | |
static async getCurrentPageUrl(page: Page) { | |
const currentLocation = await page.evaluate(() => { | |
return window.location.href | |
}) | |
console.log(`chrome.ts :: Chrome :: getCurrentPageUrl :: currentLocation :: ${currentLocation}`) | |
return currentLocation | |
} | |
static async setIphoneViewport(page: Page) { | |
console.log(`chrome.ts :: Chrome :: setIphoneViewport :: `) | |
originalViewport = page.viewportSize() | |
await page.setViewportSize({ | |
width: 390, | |
height: 844, | |
}) | |
await page.reload() | |
await page.waitForTimeout(ChromeConstants.defaultMaxWaitMs * 3) | |
} | |
static async resetViewport(page: Page) { | |
try { | |
await page.setViewportSize(originalViewport) | |
} | |
catch (e) { | |
// | |
} | |
await page.reload() | |
await page.waitForTimeout(ChromeConstants.defaultMaxWaitMs * 3) | |
} | |
// static async tryClick(page: Page, selector: string, options?: { | |
// forceClick?: boolean, | |
// }) { | |
// console.log(`chrome.ts :: Chrome :: tryClick :: selector -> ${selector} , forceClick -> ${options?.forceClick} `) | |
// try { | |
// const element = await page.$(selector) | |
// await element.click({ | |
// timeout: Constants.defaultButtonClickTimeout, | |
// delay: Constants.defaultButtonClickDelay, | |
// trial: true | |
// }) | |
// await Chrome.waitForTimeout(page) | |
// await element.click({ | |
// timeout: Constants.defaultButtonClickTimeout, | |
// delay: Constants.defaultButtonClickDelay, | |
// force: options?.forceClick, | |
// }) | |
// await Chrome.waitForTimeout(page) | |
// console.log(`chrome.ts :: Chrome :: tryClick :: Success`) | |
// return true | |
// } | |
// catch (e) { | |
// console.log(`chrome.ts :: Chrome :: tryClick :: Failed`, e) | |
// return false | |
// } | |
// } | |
static async tryClick(page: Page, selector: string, options?: { | |
forceClick?: boolean, | |
}) { | |
console.log(`chrome.ts :: Chrome :: tryClick :: selector -> ${selector} , forceClick -> ${options?.forceClick} `) | |
let isClicked = false | |
try { | |
await page.waitForSelector(selector, { | |
timeout: ChromeConstants.defaultMaxWaitMs, | |
}) | |
} | |
catch (e) { | |
console.log(`Chrome.ts :: Chrome :: e -> `, e) | |
} | |
try { | |
const element = await page.$(selector) | |
await element.click({ | |
timeout: ChromeConstants.defaultButtonClickTimeout, | |
delay: ChromeConstants.defaultButtonClickDelay, | |
trial: true | |
}) | |
isClicked = true | |
await Chrome.waitForTimeout(page) | |
} | |
catch (e) { | |
console.log(`Chrome.ts :: Chrome :: e -> `, e) | |
} | |
try { | |
const element = await page.$(selector) | |
await element.click({ | |
timeout: ChromeConstants.defaultButtonClickTimeout, | |
delay: ChromeConstants.defaultButtonClickDelay, | |
force: options?.forceClick, | |
}) | |
await Chrome.waitForTimeout(page) | |
isClicked = true | |
} | |
catch (e) { | |
console.log(`Chrome.ts :: Chrome :: e -> `, e) | |
} | |
try { | |
await page.evaluate((sel) => { | |
// @ts-ignore | |
document.querySelector(sel).click() | |
}, selector) | |
await Chrome.waitForTimeout(page) | |
isClicked = true | |
} | |
catch (e) { | |
console.log(`Chrome.ts :: Chrome :: e -> `, e) | |
} | |
return isClicked | |
} | |
static async tryClickElement(page: Page, element: any, options?: { | |
forceClick?: boolean, | |
}) { | |
try { | |
await element.click({ | |
timeout: ChromeConstants.defaultButtonClickTimeout, | |
delay: ChromeConstants.defaultButtonClickDelay, | |
trial: true | |
}) | |
await Chrome.waitForTimeout(page) | |
await element.click({ | |
timeout: ChromeConstants.defaultButtonClickTimeout, | |
delay: ChromeConstants.defaultButtonClickDelay, | |
force: options?.forceClick, | |
}) | |
await Chrome.waitForTimeout(page) | |
console.log(`chrome.ts :: Chrome :: tryClick :: Success`) | |
return true | |
} | |
catch (e) { | |
console.log(`chrome.ts :: Chrome :: tryClick :: Failed`, e) | |
return false | |
} | |
} | |
static async waitForTimeout(page: Page, options?: { | |
minTimeout?: number, | |
maxTimeout?: number, | |
}) { | |
const min = options?.minTimeout ?? ChromeConstants.defaultMinWaitMs | |
const max = options?.maxTimeout ?? ChromeConstants.defaultMaxWaitMs | |
const timeoutt = getRandomInt(min, max) | |
console.log(`chrome.ts :: Chrome :: waitForTimeout :: timeoutt -> ${timeoutt} `) | |
await page.waitForTimeout(timeoutt) | |
} | |
} |
Author
p32929
commented
Aug 20, 2024
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment