@alleyway/add-tradingview-alerts-tool
Version:
Add TradingView Alerts in bulk
827 lines (664 loc) • 33.8 kB
text/typescript
import {ISingleAlertSettings} from "../interfaces.js";
import {waitForTimeout, isEnvEnabled} from "./common-service.js";
import log from "./log.js"
import kleur from "kleur";
import {
AddAlertInvocationError,
InvalidSymbolError,
NoInputFoundError,
SelectionError,
ErrorWithScreenShot
} from "../classes.js";
import RegexParser from "regex-parser"
import {accessSync, constants, writeFileSync} from "fs";
import puppeteer, {Browser, executablePath} from "puppeteer";
import path from "path";
import {mkdir} from "fs/promises";
import exp from "constants";
// data-dialog-name="gopro"
const screenshot = isEnvEnabled(process.env.SCREENSHOT)
const doXpath = async (page, selector: string) => {
/* istanbul ignore next */
const response = await page.evaluate((sel) => {
const el = document.evaluate(sel,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue as HTMLElement
return !!el
}, selector)
return response
}
export const isXpathVisible = async (page, selector: string, screenShotOnFail = false) => {
log.debug(kleur.gray(`...isXpathVisible?: ${kleur.yellow(selector)}`))
const visible = await doXpath(page, selector)
log.debug(`..isXpathVisible: ${visible}`)
return visible
}
export const fetchFirstXPath = async (page, selector: string, timeout = 20000, screenshotOnFail = true) => {
log.debug(kleur.gray(`...selector: ${kleur.yellow(selector)}`))
try {
await page.waitForSelector("xpath/." + selector, {timeout})
} catch (e) {
if (screenshotOnFail) await takeScreenshot(page, "waitForSelectorFailed")
throw (e)
}
const elements = await page.$$("xpath/." + selector)
return elements[0]
}
export const takeScreenshot = async (page, name: string = "unnamed") => {
if (screenshot) {
const username = await page.evaluate(() => {
// @ts-ignore
const user = window.user
if (user) {
return "_" + user.username
} else {
return ""
}
})
const screenshotPath = `screenshot_${new Date().getTime()}${username}_${name}`
log.debug(`saving screenshot: ${screenshotPath}`)
await page.screenshot({
path: screenshotPath + ".webp",
});
const html = await page.content()
writeFileSync(screenshotPath + ".xml", html, {encoding: "utf-8"})
}
}
export const minimizeFooterChartPanel = async (page) => {
log.debug(`minimizing footer chart panel`)
try {
const footerPanelMinimizeButton = await fetchFirstXPath(page, `//div[@id='footer-chart-panel']//button[@data-name='toggle-visibility-button' and @data-active='false']`, 5000)
footerPanelMinimizeButton.click()
await waitForTimeout(.4);
} catch (e) {
log.debug("no minimize button found, footer chart panel must be hidden already")
}
}
export const convertIntervalForTradingView = (interval: string) => {
return interval.split("").filter((val) => val !== "m").join("")
}
export const configureInterval = async (interval: string, page) => {
log.debug(`set ${kleur.blue("interval")}: ${kleur.yellow(interval)}`)
await page.keyboard.press(",")
await waitForTimeout(.5, "after pressing interval shortcut key");
try {
convertIntervalForTradingView(interval).split("").map((char) => page.keyboard.press(char))
} catch (e) {
throw Error("configuration: interval specified incorrectly, should be something like '5m' or '1h' - see documentation")
}
await page.keyboard.press('Enter')
}
// queries used on the alert conditions
const dropdownXpathQueries = {
primaryLeft: "//span[@data-name='main-series-select']",
primaryRight: "//span[@data-name='main-series-plot-select']",
secondary: "//span[@data-name='operator-select']",
// Only get listbox when there's either just one value
// "//*[contains(@class, 'operatorRow-')]/..//span[@data-name='start-band-select']"
// Or get when there's an upper and lower bound
// "//legend[text()='Upper Bound']/../..//span[@data-name='start-band-select']"
tertiaryLeft: "//*[contains(@class, 'operatorRow-')]/..//span[@data-name='start-band-select'] | //legend[text()='Upper bound']/../..//span[@data-name='start-band-select']",
tertiaryRight: "//*[contains(@class, 'operatorRow-')]/..//span[@data-name='end-band-select'] | //legend[text()='Upper bound']/../..//span[@data-name='end-band-select']",
quaternaryLeft: "//legend[text()='Lower bound']/../..//span[@data-name='start-band-select']",
quaternaryRight: "//legend[text()='Lower bound']/../..//span[@data-name='end-band-select']",
}
const dropdownSoundXpathQueries = {
nameTarget: "//button[@role='button' and @data-name='sound-title-select']",
classicSection: "//button[@role='group' and @aria-expanded='false']//div[text()='Classic']",
nameListItems: "//div[@data-name='popup-menu-container']//div[@role='option']//div[contains(@class, 'title-')]//div[contains(@class, 'title-')]",
durationTarget: "//button[@data-name='sound-duration-select']",
durationListItems: "//div[@data-name='popup-menu-container']//div[@role='option']//div[contains(@class, 'title-')]",
}
const inputXpathQueries = {
tertiaryLeft: "//*[contains(@class, 'operatorRow-')]/..//input[@data-property-id='start-band-range'] | //legend[text()='Upper bound']/../..//input[@data-property-id='start-band-range']",
tertiaryRight: "//*[contains(@class, 'operatorRow-')]/..//input[@data-property-id='end-band-range'] | //legend[text()='Upper bound']/../..//input[@data-property-id='end-band-range']",
quaternaryLeft: "//*[contains(@class, 'operatorRow-')]/..//input[@data-property-id='start-band-range'] | //legend[text()='Lower bound']/../..//input[@data-property-id='start-band-range']",
quaternaryRight: "//*[contains(@class, 'operatorRow-')]/..//input[@data-property-id='end-band-range'] | //legend[text()='Lower bound']/../..//input[@data-property-id='end-band-range']",
}
const readOnlyInputQueries = {
tertiaryLeft: "(//div[contains(@class, 'tv-alert-dialog__group-item--left ') and contains(@class, 'js-second-operand-')])[1]//input[@type='text']",
quaternaryLeft: "(//div[contains(@class, 'tv-alert-dialog__group-item--left ') and contains(@class, 'js-second-operand-')])[2]//input[@type='text']"
}
const alertActionCorresponding = {
notifyOnApp: "notify-on-app",
showPopup: "show-popup",
sendEmail: "send-email",
webhook: "webhook",
playSound: "play-sound"
}
const clickInputAndDelete = async (page, inputElement) => {
await inputElement.click()
await waitForTimeout(.5)
/* istanbul ignore next */
await page.evaluate((el) => {
el.value = ""
}, inputElement)
await waitForTimeout(.3)
}
export const launchBrowser = async (headless: boolean, url?: string): Promise<Browser> => {
const userDataDir = path.join(process.cwd(), "user_data") // where chrome will store it's stuff
try {
accessSync(userDataDir, constants.W_OK)
} catch {
log.info(`Attempting to create directory for Chrome user data\n ${kleur.yellow(userDataDir)}`)
await mkdir(userDataDir)
}
return puppeteer.launch({
executablePath: executablePath(),
headless: headless, userDataDir,
defaultViewport: {width: 1920, height: 1080, isMobile: false, hasTouch: false},
args: ['--no-sandbox',
'--enable-experimental-web-platform-features', // adds support for :has selector in styleOverrides. In theory its not experimental in chrome 105
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
// '--single-process', // will cause it to die
'--disable-gpu',
headless && !url ? "" : `--app=${url}`,
'--window-size=1920,1080', // otherwise headless doesn't work
// '--incognito'
]
})
}
export const login = async (page, username, pass, backupCode) => {
try {
const emailSignInButton = await fetchFirstXPath(page, `//div[@data-dialog-name='sign-in']//button[@name='Email']`, 5000)
emailSignInButton.click()
await waitForTimeout(1);
} catch (e) {
log.warn("no email toggle button showing!")
}
const usernameInput = await fetchFirstXPath(page, '//input[@name=\'id_username\']')
await usernameInput.type(`${username}`)
await waitForTimeout(.5);
await takeScreenshot(page, "shouldbe_before_password_entry")
const passwordInput = await fetchFirstXPath(page, '//input[@name=\'id_password\']')
log.debug("typing password")
await passwordInput.type(pass)
await waitForTimeout(.5);
const submitButton = await fetchFirstXPath(page, "//div[@data-dialog-name='sign-in']//button[contains(@class, 'submitButton')]")
log.debug("clicking submit button")
submitButton.click()
await waitForTimeout(2);
let twoFactorOrBackupCodeInput = null
try {
twoFactorOrBackupCodeInput = await fetchFirstXPath(page, "//div[@data-dialog-name='sign-in']//input[@id='id_code']", 5000)
} catch (e) {
log.warn("Two Factor Authentication / Backup Code input not showing")
}
if (twoFactorOrBackupCodeInput !== null) {
if (!backupCode) {
log.warn(`${kleur.yellow("MANUAL ACTION NEEDED: ")} You have 30 seconds to enter your 2FA or backup code`)
await waitForTimeout(30 * 1000);
} else {
await twoFactorOrBackupCodeInput.type(`${backupCode}`)
await waitForTimeout(3);
}
}
let possibleErrorElement = null
try {
possibleErrorElement = await fetchFirstXPath(page,
"//div[@data-dialog-name='sign-in']//div[contains(@class, 'mainProblem')]//div[contains(@class,'text-wrap')]/span",
5000)
} catch (e) {
log.warn("doesn't seem like there's an error..")
}
if (possibleErrorElement) {
const errorText = await page.evaluate(element => element.innerText, possibleErrorElement);
await takeScreenshot(page, "loginError")
throw new ErrorWithScreenShot(errorText, "loginError")
}
}
export const logout = async (page) => {
/* istanbul ignore next */
await page.evaluate(() => {
fetch("/accounts/logout/", {
method: "POST",
headers: {accept: "html"},
credentials: "same-origin"
}).then(res => {
log.info(`Logged out of TradingView`)
});
})
await page.reload({
waitUntil: 'networkidle2'
});
}
export const checkForInvalidSymbol = async (page, symbol: string) => {
// this function could be used when navigating by typing or by the url using ?symbol=ASDF
// now see if it's invalid symbol, could be multi-chart so check active
if (await isXpathVisible(page, "//div[contains(@class,'chart-container') and contains(@class,' active')]//*/div[contains(@class, 'invalidSymbol') and not(contains(@class, 'js-hidden'))]")) {
log.error("currently showing an invalid symbol")
const invalidSymbolError = new InvalidSymbolError()
invalidSymbolError.symbol = symbol
throw invalidSymbolError
}
}
export const navigateToSymbol = async (page, symbol: string) => {
await page.keyboard.press('Escape')
await waitForTimeout(.5);
await page.keyboard.press('Escape')
await waitForTimeout(.5);
await page.keyboard.type(`A`, {delay: 0.3}) // just type a letter <- allows formulas to work, eg. 1/USD...
await page.keyboard.press('Backspace');
await waitForTimeout(.3);
await page.keyboard.type(`${symbol}`, {delay: 0.3})
await waitForTimeout(.3);
await page.keyboard.press('Enter')
await waitForTimeout(1.5);
}
const isMatch = (needle: string, haystack: string) => {
if (needle.startsWith("/")) {
log.debug(`Parsing what appears to be regular expression: ${kleur.yellow(needle)} ... Haystack: ${kleur.gray(haystack)}`)
const regexp: RegExp = RegexParser(needle)
return !!regexp.exec(haystack)
} else {
return haystack.indexOf(needle) > -1
}
}
export const configureSingleAlertSettings = async (page, singleAlertSettings: ISingleAlertSettings) => {
const {condition, name, expireOpenEnded, expireInterval, option, message, actions} = singleAlertSettings
await takeScreenshot(page, "alert_begin_configure")
try {
const gotItButton = await fetchFirstXPath(page, "//button//span[contains(@data-overflow-tooltip-text, 'Got it')]", 1000, false)
log.warn("Clicking 'Got it' informational bubble...")
gotItButton.evaluate(e => e.click())
} catch (e) {
log.trace("Checked for 'Got it' button, but found nothing")
}
const selectFromDropDown = async (conditionToMatchArg, selector: string) => {
log.debug(`..selectFromDropDown() using selector: ${kleur.yellow(selector)}`)
await page.waitForSelector("xpath/." + selector, {timeout: 8000})
const elements = await page.$$("xpath/." + selector)
if (elements.length == 0) {
log.warn("zero dropdown options found with selector")
await takeScreenshot(page, "zero_dropdown_options")
}
let conditionToMatch = conditionToMatchArg
let targetOccurrence = 0
const match = conditionToMatch.match(/(.*?)\[(\d+)\]$/)
// if match is not null, then the number to look for should be match[1]
if (match) {
conditionToMatch = match[1]
targetOccurrence = Number.parseInt(match[2])
log.debug(`Indexed condition used: ${kleur.yellow(conditionToMatchArg)}\n Setting occurrence to ${kleur.blue(targetOccurrence)}`)
}
log.debug(`searching menu for ${kleur.yellow(conditionToMatch)}`)
let found = false
let foundOptions = []
let occurrenceCount = 0
for (const el of elements) {
/* istanbul ignore next */
let optionText = await page.evaluate(element => element.innerText, el);
optionText = optionText.replace(/[\u200B]/g, ''); // this is to remove invisible "zero width" characters like for the following:
// Loner S/R (modified, 28, 5, Standard, -20, modified, 21, 3, 40, 10, 20, 5, 64, 1.5, both)
foundOptions.push(optionText)
if (isMatch(conditionToMatch, optionText)) {
if (occurrenceCount == targetOccurrence) {
log.debug(`Found! Clicking ${kleur.yellow(optionText)}`)
found = true
el.click()
return;
} else {
log.debug(`Matching option found, but not occurrenceCount ${kleur.blue(occurrenceCount)} not matching targetOccurrence `)
occurrenceCount += 1
}
}
}
if (!found) throw new SelectionError(conditionToMatch, foundOptions)
}
const performActualEntry = async (key) => {
const conditionOrInputValue = String(condition[key]);
log.debug(`Processing ${kleur.blue(key)}: ${kleur.yellow(conditionOrInputValue)}`)
await waitForTimeout(.8);
if (conditionOrInputValue !== "null" && String(conditionOrInputValue).length > 0) {
try {
log.debug(`Looking for DROPDOWN xpath of ${kleur.yellow(key)}`)
const targetElement = await fetchFirstXPath(page, dropdownXpathQueries[key], 3000)
// must be a dropdown...
log.debug(`Found dropdown! Clicking element of ${kleur.yellow(key)}`)
targetElement.click()
await waitForTimeout(.9, "let dropdown populate");
await selectFromDropDown(conditionOrInputValue, "//div[@data-name='popup-menu-container']//div[@role='option']/span/span/div/div/span")
await waitForTimeout(.4, "after selecting from dropdown");
} catch (e) {
if (e.constructor.name === "TimeoutError") {
if (!inputXpathQueries[key]) throw (new NoInputFoundError(`Unable to find dropdown xpath target for primaryLeft/secondary. Make sure chart layout is SAVED with an indicator that contains/matches this: ${conditionOrInputValue}`))
log.debug(`Timed out looking for dropdown. Looking for INPUT xpath of ${kleur.yellow(key)}`)
try {
const valueInput = await fetchFirstXPath(page, inputXpathQueries[key], 1000)
log.debug(`Typing value: ${kleur.blue(conditionOrInputValue)}`)
await clickInputAndDelete(page, valueInput)
await valueInput.type(String(conditionOrInputValue))
} catch (inputError) {
if (inputError.constructor.name === "TimeoutError") {
if (!readOnlyInputQueries[key]) throw (new NoInputFoundError(`Unable to find 'readonlyInput' xpath target for ${key} which doesn't have inputs, so won't even try`))
log.debug(`Timed out looking for input. Looking for READ-ONLY INPUT xpath of ${kleur.yellow(key)}`)
try {
const valueReadonlyInput = await fetchFirstXPath(page, readOnlyInputQueries[key], 1000)
/* istanbul ignore next */
const readOnlyValue = await page.evaluate((el) => el.value, valueReadonlyInput)
if (readOnlyValue === conditionOrInputValue) {
log.debug(`looks like the readonly input is actually ${conditionOrInputValue} as expected`)
} else {
throw new Error(`Read only input value for ${key} is ${readOnlyValue}, but expected ${conditionOrInputValue}`)
}
} catch (readOnlyInputError) {
if (readOnlyInputError.constructor.name === "TimeoutError") {
throw new NoInputFoundError(`Unable to find any inputs for '${key}' for configured value: '${conditionOrInputValue}'`)
} else {
throw readOnlyInputError
}
}
} else {
throw inputError
}
}
} else {
throw e
}
}
}
}
await performActualEntry("primaryLeft")
try {
await performActualEntry("primaryRight")
await performActualEntry("secondary")
} catch (e) {
if (e instanceof NoInputFoundError) {
log.debug("NoInputFoundError, maybe we need to send secondary before setting primaryRight")
// sometimes the secondary must be set first before the primaryRight shows up
await performActualEntry("secondary")
await performActualEntry("primaryRight")
} else {
throw e
}
}
await performActualEntry("tertiaryLeft")
await performActualEntry("tertiaryRight")
await performActualEntry("quaternaryLeft")
await performActualEntry("quaternaryRight")
await waitForTimeout(.4);
// if (!!option) {
// log.debug(`Looking for option: ${kleur.blue(option)}`)
//
// try {
//
// const targetElement = await fetchFirstXPath(page, "//fieldset[@aria-label='Trigger']//span[@role='button']", 3000)
// log.debug(`Found dropdown! Clicking element of ${kleur.yellow(option)}`)
// targetElement.evaluate((b) => b.click())
// await waitForTimeout(.3)
// await selectFromDropDown(option, "//div[@data-name='popup-menu-container']//div[@role='option']/span/span/div")
//
// } catch (e) {
// if (e.constructor.name === "TimeoutError") {
// throw new Error(`No fire rate 'option' available, but one was specified in alert configuration: ${option}`)
// } else {
// throw e
// }
// }
//
// }
if (!!option) {
log.debug(`Looking for option: ${kleur.blue(option)}`)
const selector = "//legend[text()='Trigger']/../..//button//span[contains(@class,'ellipsisContainer')]"
try {
await page.waitForSelector("xpath/." + selector, {timeout: 8000})
} catch (e) {
if (e.constructor.name === "TimeoutError") {
throw new Error(`No fire rate 'option' available, but one was specified in alert configuration: ${option}`)
} else {
throw e
}
}
const elements = await page.$$("xpath/." + selector)
let found = false
let foundOptions = []
for (const el of elements) {
/* istanbul ignore next */
let optionText = await page.evaluate(element => element.innerText, el);
foundOptions.push(optionText.trim())
if (optionText === option && !found) {
log.debug(`Found! Clicking ${kleur.yellow(optionText)}`)
found = true
await waitForTimeout(.4);
await el.click()
// /* istanbul ignore next */
// await page.evaluate((text) => {
// const el = document.evaluate(`//*[@class='js-fire-rate-row']//div[@data-title='${text}']`,
// document,
// null,
// XPathResult.FIRST_ORDERED_NODE_TYPE,
// null
// ).singleNodeValue as HTMLElement
// el.click()
//
// }, option)
await waitForTimeout(.4);
// const justClickedEl = await fetchFirstXPath(page, `//*[@class='js-fire-rate-row']//div[@data-title='${option}']/..`)
//
// /* istanbul ignore next */
// const className = await page.evaluate(el => el.className, justClickedEl);
//
// if (className.indexOf("i-active") < 0) {
// log.error("option element was clicked, but it's parent does not have the 'i-active' class assigned")
// throw Error("Unable to select option correctly...a bug in the system")
// }
}
}
if (!found) throw new SelectionError(option, foundOptions)
}
if (expireOpenEnded !== undefined) {
log.debug("set whether to expire open ended")
await waitForTimeout(.1);
const expDropdownTarget = await fetchFirstXPath(page, "//legend[text()='Expiration']/../..//button")
await expDropdownTarget.click()
await waitForTimeout(.4);
const openEndedCheckbox = await fetchFirstXPath(page, `//input[@id='unexpired-date']`)
const isOpenEnded = await page.evaluate(element => element.checked, openEndedCheckbox)
if (expireOpenEnded != isOpenEnded) {
openEndedCheckbox.click()
}
await waitForTimeout(.3);
if (!expireOpenEnded && expireInterval) {
log.info(`Set Expiration ${kleur.blue(expireInterval)} hours in the future`)
const dateInput = await fetchFirstXPath(page, "//div[contains(@class, 'pickerInput-')]//input")
const timeInput = await fetchFirstXPath(page, "//div[contains(@class, 'time-')]//input")
const dateISO = await page.evaluate((expireInterval) => {
console.log("expireInterval: " + expireInterval)
const currentDate = new Date()
currentDate.setTime(currentDate.getTime() - (currentDate.getTimezoneOffset() * 60000) + expireInterval * 60 * 60 * 1000);
return currentDate.toISOString()
}, expireInterval)
const [splitDate, timeFull] = dateISO.split("T")
const [hours, min, secs] = timeFull.split(":")
const expDate = splitDate.replaceAll("-", "")
const expTime = `${hours}${min}`
log.debug(`exp_date: ${expDate}`)
log.debug(`exp_time: ${expTime}`)
await dateInput.click()
await waitForTimeout(50);
await page.keyboard.press('End');
for (const l of "yyyy-mm-dd".split("")) {
await waitForTimeout(50);
await page.keyboard.press('Backspace');
}
await dateInput.type(String(expDate))
await waitForTimeout(50);
await timeInput.click()
await waitForTimeout(50);
await page.keyboard.press('End');
for (const l of "hh:mm".split("")) {
await waitForTimeout(50);
await page.keyboard.press('Backspace');
}
await timeInput.type(String(expTime))
await waitForTimeout(.2);
}
await waitForTimeout(.2);
const expSubmitButton = await fetchFirstXPath(page, "//div[@data-name='popup-menu-container']//div[@data-name='menu-inner']/div/button")
await expSubmitButton.click()
}
if (!!name) {
log.debug(`Setting Alert Name: ${kleur.blue(name)}`)
try {
const customAlertNameButton = await fetchFirstXPath(page, "//button[@id='alert-name']", 1000, false)
log.warn("Clicking custom alert name button")
customAlertNameButton.evaluate(e => e.click())
} catch (e) {
log.trace("Tried to click custom alert name button, but got nothing")
}
const nameInput = await fetchFirstXPath(page, "//input[@id='alert-name']")
await clickInputAndDelete(page, nameInput)
await nameInput.type(name)
await waitForTimeout(.5);
}
if (!!message) {
log.debug(`Setting message: ${kleur.blue(message)}`)
try {
const messageTextarea = await fetchFirstXPath(page, "//textarea[@id='alert-message']", 1000, false)
await clickInputAndDelete(page, messageTextarea)
await messageTextarea.type(message)
} catch (e) {
if (e.constructor.name === "TimeoutError") {
log.warn(`No message box found, but message specified: ${message}`)
} else {
throw e
}
}
}
await waitForTimeout(.2);
const notificationsTab = await fetchFirstXPath(page, "//button[@id='alert-dialog-tabs__notifications']")
await notificationsTab.click()
await waitForTimeout(.4);
// alert actions
for (const [configKey, elementInputName] of Object.entries(alertActionCorresponding)) {
if (!!actions && !!actions[configKey] !== undefined) {
await waitForTimeout(.3)
const el = await fetchFirstXPath(page, `//input[@data-name='${elementInputName}']`)
/* istanbul ignore next */
const isChecked = await page.evaluate(element => element.checked, el)
if (configKey === "webhook") {
if (isChecked != actions.webhook.enabled) {
log.debug(`setting ${kleur.blue("webhook")} as checked`)
el.click()
await waitForTimeout(.3)
}
if (actions.webhook.enabled && actions.webhook.url) {
await waitForTimeout(.3)
log.debug(`typing webhook url: ${kleur.blue(actions.webhook.url)}`)
const webhookUrlEl = await fetchFirstXPath(page, `//input[contains(@placeholder, 'alert-hook')]`, 1000)
await clickInputAndDelete(page, webhookUrlEl)
await webhookUrlEl.type(String(actions.webhook.url))
}
} else if (configKey === "playSound") {
if (actions.playSound?.enabled !== undefined && isChecked != actions.playSound?.enabled) {
log.debug(`setting ${kleur.blue("play-sound")} input as checked`)
el.click()
await waitForTimeout(.3)
}
if (actions.playSound?.enabled && actions.playSound?.name && actions.playSound?.duration) {
{
await waitForTimeout(.5)
try {
const classicSoundsSectionButton = await fetchFirstXPath(page, dropdownSoundXpathQueries["classicSection"], 3000, false)
classicSoundsSectionButton.evaluate((b) => b.click())
log.trace("Clicked to expand 'Classic' sounds")
} catch (e) {
log.trace("'Classic' sounds section already open")
}
await waitForTimeout(.1)
log.debug(`Looking for DROPDOWN xpath of ${kleur.yellow("playSound.name")}`)
const targetElement = await fetchFirstXPath(page, dropdownSoundXpathQueries["nameTarget"], 3000)
log.debug(`Found dropdown! Clicking element of ${kleur.yellow(actions.playSound.name)}`)
targetElement.evaluate((b) => b.click())
await waitForTimeout(.3)
await selectFromDropDown(actions.playSound.name, dropdownSoundXpathQueries["nameListItems"])
}
{
await waitForTimeout(.5)
log.debug(`Looking for DROPDOWN xpath of ${kleur.yellow("playSound.duration")}`)
const targetElement = await fetchFirstXPath(page, dropdownSoundXpathQueries["durationTarget"], 3000)
log.debug(`Found dropdown! Clicking element of ${kleur.yellow(actions.playSound.duration)}`)
targetElement.evaluate((b) => b.click())
await waitForTimeout(.3)
await selectFromDropDown(actions.playSound.duration, dropdownSoundXpathQueries["durationListItems"])
}
await waitForTimeout(.5)
}
} else {
if (isChecked != actions[configKey]) {
log.debug(`setting ${kleur.blue(configKey)} as checked`)
el.click()
}
}
}
}
}
export const clickSubmit = async (page) => {
log.debug("clickSubmit()")
const submitButton = await fetchFirstXPath(page, "//div[contains(@data-name, 'alerts-create-edit-dialog')]//button[@data-name='submit']")
submitButton.evaluate((b) => b.click())
}
// sometimes there's a warning of "this alert may trigger differently than expected"
export const clickContinueIfWarning = async (page) => {
try {
log.debug("clickContinueIfWarning()")
const continueAnywayButton = await fetchFirstXPath(page, `//button[@name='continue']`,
3000, false)
continueAnywayButton.evaluate((b) => b.click())
await waitForTimeout(4, "waiting after clicking 'continue anyway' button");
} catch (error) {
log.debug("No warning dialog")
}
}
export const addAlert = async (page, singleAlertSettings: ISingleAlertSettings) => {
log.debug("addAlert()")
const typeShortcutForAlertDialog = async () => {
log.debug("addAlert()...pressing shortcut key")
await page.keyboard.press('Escape')
await waitForTimeout(.5);
await page.keyboard.press('Escape')
await waitForTimeout(.5);
await page.keyboard.down('AltLeft')
await page.keyboard.press("a")
await page.keyboard.up('AltLeft')
await waitForTimeout(.5, "after keyboard shortcut for new alert dialog");
}
await typeShortcutForAlertDialog()
await waitForTimeout(.7, "..make sure we're showing the alert dialog");
const isNotShowingAlertDialog = async () => {
return !(await isXpathVisible(page, "//div[contains(@data-name, 'alerts-create-edit-dialog')]"))
}
if (await isNotShowingAlertDialog()) {
log.warn("NOT showing alert dialog! maybe invalid symbol?")
if (await isXpathVisible(page, "//*[text()=\"Can't create alert on invalid symbol\"]")) {
log.error("Looks like we tried to create alert on invalid symbol, throwing error")
throw new InvalidSymbolError()
}
const MAX_TRIES = 3
let retryCount = 1
while ((await isNotShowingAlertDialog()) && retryCount <= MAX_TRIES + 1) {
if (retryCount == MAX_TRIES) {
await takeScreenshot(page, "unable_to_bring_up_alert_dialog")
throw new AddAlertInvocationError()
}
log.debug("Attempting to show alert dialog again...")
await waitForTimeout(retryCount, "pausing a little")
await typeShortcutForAlertDialog()
retryCount += 1
}
}
await configureSingleAlertSettings(page, singleAlertSettings)
await waitForTimeout(.5);
await takeScreenshot(page, "before_submitting_alert")
await clickSubmit(page)
await waitForTimeout(2);
await clickContinueIfWarning(page)
await waitForTimeout(2, "waiting a little after adding")
}