Skip to content

Instantly share code, notes, and snippets.

@p32929
Last active September 7, 2024 15:44
Show Gist options
  • Save p32929/7a2375cf2eb3d2986a741d7dc293a4c8 to your computer and use it in GitHub Desktop.
Save p32929/7a2375cf2eb3d2986a741d7dc293a4c8 to your computer and use it in GitHub Desktop.
Playwright utility functions
// 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)
}
}
@p32929
Copy link
Author

p32929 commented Aug 20, 2024

import { Page } from "playwright"
import { Chrome } from "./Chrome"

export class ChromeGlobals {
    public static readonly emojis = [
        "๐Ÿค”๐Ÿค”๐Ÿค”", "๐Ÿ™ƒ๐Ÿ™ƒ๐Ÿ™ƒ", "๐Ÿ˜๐Ÿ˜๐Ÿ˜", "๐Ÿคทโ€โ™‚๏ธ๐Ÿคทโ€โ™‚๏ธ๐Ÿคทโ€โ™‚๏ธ", "๐Ÿคทโ€โ™€๏ธ๐Ÿคทโ€โ™€๏ธ๐Ÿคทโ€โ™€๏ธ",
        "๐Ÿ˜‘๐Ÿ˜‘๐Ÿ˜‘", "๐Ÿ˜ถ๐Ÿ˜ถ๐Ÿ˜ถ", "๐Ÿ™„๐Ÿ™„๐Ÿ™„", "๐Ÿคจ๐Ÿคจ๐Ÿคจ", "๐Ÿ˜•๐Ÿ˜•๐Ÿ˜•",
        "๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ", "๐Ÿ˜ณ๐Ÿ˜ณ๐Ÿ˜ณ", "๐Ÿ˜ฒ๐Ÿ˜ฒ๐Ÿ˜ฒ", "๐Ÿค๐Ÿค๐Ÿค", "๐Ÿ˜ฎ๐Ÿ˜ฎ๐Ÿ˜ฎ",
        "๐Ÿ˜ต๐Ÿ˜ต๐Ÿ˜ต", "๐Ÿค”๐Ÿค”๐Ÿค”", "๐Ÿ˜ฏ๐Ÿ˜ฏ๐Ÿ˜ฏ", "๐Ÿ˜ฌ๐Ÿ˜ฌ๐Ÿ˜ฌ", "๐Ÿ˜Ÿ๐Ÿ˜Ÿ๐Ÿ˜Ÿ",
        "๐Ÿ˜ฆ๐Ÿ˜ฆ๐Ÿ˜ฆ", "๐Ÿคซ๐Ÿคซ๐Ÿคซ", "๐Ÿคท๐Ÿคท๐Ÿคท", "๐Ÿ˜ถโ€๐ŸŒซ๏ธ๐Ÿ˜ถโ€๐ŸŒซ๏ธ๐Ÿ˜ถโ€๐ŸŒซ๏ธ", "๐Ÿ˜Œ๐Ÿ˜Œ๐Ÿ˜Œ",
        "๐Ÿง๐Ÿง๐Ÿง", "๐Ÿ˜ด๐Ÿ˜ด๐Ÿ˜ด", "๐Ÿ˜Œ๐Ÿ˜Œ๐Ÿ˜Œ", "๐Ÿ˜ง๐Ÿ˜ง๐Ÿ˜ง", "๐Ÿ™ƒ๐Ÿ™ƒ๐Ÿ™ƒ",
        "๐Ÿ˜ตโ€๐Ÿ’ซ๐Ÿ˜ตโ€๐Ÿ’ซ๐Ÿ˜ตโ€๐Ÿ’ซ", "๐Ÿคช๐Ÿคช๐Ÿคช", "๐Ÿ˜•๐Ÿ˜•๐Ÿ˜•", "๐Ÿ˜‘๐Ÿ˜‘๐Ÿ˜‘", "๐Ÿคฆโ€โ™‚๏ธ๐Ÿคฆโ€โ™‚๏ธ๐Ÿคฆโ€โ™‚๏ธ",
        "๐Ÿคฆโ€โ™€๏ธ๐Ÿคฆโ€โ™€๏ธ๐Ÿคฆโ€โ™€๏ธ", "๐Ÿคฅ๐Ÿคฅ๐Ÿคฅ", "๐Ÿ˜ฅ๐Ÿ˜ฅ๐Ÿ˜ฅ", "๐Ÿ˜๐Ÿ˜๐Ÿ˜", "๐Ÿ˜ช๐Ÿ˜ช๐Ÿ˜ช",
        "๐Ÿ˜ง๐Ÿ˜ง๐Ÿ˜ง", "๐Ÿคฏ๐Ÿคฏ๐Ÿคฏ", "๐Ÿค”๐Ÿค”๐Ÿค”", "๐Ÿ™๐Ÿ™๐Ÿ™", "๐Ÿ˜ฆ๐Ÿ˜ฆ๐Ÿ˜ฆ",
        "๐Ÿ™„๐Ÿ™„๐Ÿ™„", "๐Ÿ˜’๐Ÿ˜’๐Ÿ˜’", "๐Ÿ˜ฌ๐Ÿ˜ฌ๐Ÿ˜ฌ", "๐Ÿ˜Ÿ๐Ÿ˜Ÿ๐Ÿ˜Ÿ", "๐Ÿค”๐Ÿค”๐Ÿค”"
    ]

    public static chrome: Chrome = null
    public static initPage: Page = null
    public static secondPage: Page = null
    public static aiPage: Page = null

    private static getChrome = () => {
        if (this.chrome === null) {
            this.chrome = new Chrome({
                browser: "firefox",
                mode: "sessioned",
            })
        }

        return this.chrome
    }

    public static getInitPage = async () => {
        if (this.initPage === null) {
            ChromeGlobals.initPage = await this.getChrome().getNewPage()
        }
        return ChromeGlobals.initPage
    }

    public static getSecondPage = async () => {
        if (this.secondPage === null) {
            ChromeGlobals.secondPage = await this.getChrome().getNewPage()
        }
        return ChromeGlobals.secondPage
    }

    public static getAiPage = async () => {
        if (this.aiPage === null) {
            ChromeGlobals.aiPage = await this.getChrome().getNewPage()
        }
        return ChromeGlobals.aiPage
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment