tridactyl.tridactyl/e2e_tests/utils.ts
Oliver Blanthorn 11693da62e
Some checks are pending
e2e / test (push) Waiting to run
lint / lint (mozilla) (push) Waiting to run
lint / lint (unit) (push) Waiting to run
lint / commit-emails (push) Waiting to run
lint / lint (lint) (push) Waiting to run
Website / build (push) Waiting to run
Website / deploy (push) Blocked by required conditions
Fix broken CI in FF155+
Flag needed to use Selenium ExecuteScript on WebExtension pages
2026-09-07 17:11:58 +02:00

181 lines
6 KiB
TypeScript

import { promises as fs } from "fs"
import * as path from "path"
import * as os from "os"
import * as process from "process"
import { Browser, Builder, By, Key, WebDriver } from "selenium-webdriver"
import { Driver, Options, ServiceBuilder } from "selenium-webdriver/firefox"
import * as Until from "selenium-webdriver/lib/until"
const env = process.env
const drivers = new Set<Driver>()
/** Returns the path of the newest file in directory */
export async function getNewestFileIn(directory: string): Promise<string> {
try {
// Get list of files
const names = await fs.readdir(directory)
// Get their stat structs
const stats = await Promise.all(
names.map(async name => {
const filePath = path.join(directory, name)
const stat = await fs.stat(filePath)
return { path: filePath, mtime: stat.mtime }
}),
)
// Sort by most recent and return the path of the newest file
return stats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0]
?.path
} catch (error) {
console.error(`Error reading directory ${directory}:`, error)
throw new Error("Couldn't find extension path")
}
}
export async function iframeLoaded(driver: Driver) {
return driver.wait(Until.elementLocated(By.id("cmdline_iframe")))
}
export async function getDriver() {
const extensionPath = await getNewestFileIn(
path.resolve("web-ext-artifacts"),
)
const options = new Options()
if (!env["HEADED"]) {
options.addArguments("--headless")
}
const driver = new Builder()
.forBrowser(Browser.FIREFOX)
.setFirefoxOptions(options)
// Required to evaluate scripts in extension pages; only for test browsers.
.setFirefoxService(new ServiceBuilder().addArguments("--allow-system-access"))
.build() as unknown as Driver
drivers.add(driver)
// This will be the default tab.
await driver.installAddon(extensionPath, true)
// Wait for multiple window handles to be available (extension may open in new tab)
await driver.wait(async () => {
const handles = await driver.getAllWindowHandles()
return handles.length >= 2
}, 10000)
// Give the extension a bit more time to initialize
await driver.sleep(500)
let handles = await driver.getAllWindowHandles()
// Handle edge case where extension loads in same tab (some headless configurations)
if (handles.length === 1) {
await driver.wait(async () => {
const newHandles = await driver.getAllWindowHandles()
return newHandles.length >= 2
}, 10000)
handles = await driver.getAllWindowHandles()
}
// Kill the original tab.
await driver.switchTo().window(handles[0])
await driver.close()
// Switch to the new tab (extension opens in new tab)
await driver.switchTo().window(handles[1])
await driver.wait(() => driver.executeScript<boolean>("return Boolean(window.tri)"), 10000)
// Now return the window that we want to use.
return driver
}
export async function getDriverAndProfileDirs() {
const rootDir = os.tmpdir()
// First, find out what profile the driver is using
const profiles = (await fs.readdir(rootDir)).map(p => path.join(rootDir, p))
const driver = await getDriver()
const newProfiles = (await fs.readdir(rootDir))
.map(p => path.join(rootDir, p))
.filter(p => p.match("moz") && !profiles.includes(p))
// Tridactyl's tmp profile detection is broken on windows and OSX
if (["win32", "darwin"].includes(os.platform())) {
await sendKeys(driver, `:set profiledir ${newProfiles[0]}<CR>`)
await driver.sleep(1000)
}
return { driver, newProfiles }
}
export async function quitDrivers() {
const results = await Promise.allSettled(
[...drivers].map(driver =>
driver.quit().finally(() => drivers.delete(driver)),
),
)
const failure = results.find(result => result.status === "rejected")
if (failure?.status === "rejected") {
throw failure.reason
}
}
const vimToSelenium = {
Down: Key.ARROW_DOWN,
Left: Key.ARROW_LEFT,
Right: Key.ARROW_RIGHT,
Up: Key.ARROW_UP,
BS: Key.BACK_SPACE,
Del: Key.DELETE,
End: Key.END,
CR: Key.ENTER,
Esc: Key.ESCAPE,
Home: Key.HOME,
PageDown: Key.PAGE_DOWN,
PageUp: Key.PAGE_UP,
Tab: Key.TAB,
lt: "<",
}
const modToSelenium = {
A: Key.ALT,
C: Key.CONTROL,
M: Key.META,
S: Key.SHIFT,
}
export async function sendKeys(driver: WebDriver, keys: string) {
const delay = 100
async function sendSingleKey(key: string) {
await driver.actions().sendKeys(key).perform()
await driver.sleep(delay)
}
async function sendSpecialKey(specialKey: string) {
const noBrackets = specialKey.slice(1, -1)
if (noBrackets.includes("-")) {
const [modifiers, key] = noBrackets.split("-")
const mods = modifiers.split("").map(mod => modToSelenium[mod])
let actions = driver.actions()
for (const mod of mods) {
actions = actions.keyDown(mod)
}
actions = actions.sendKeys(vimToSelenium[key] || key)
for (const mod of mods) {
actions = actions.keyUp(mod)
}
await actions.perform()
} else {
await sendSingleKey(vimToSelenium[noBrackets] || noBrackets)
}
}
keys = keys.replace(":", "<S-;>")
const regexp = /<[^>-]+-?[^>]*>/g
const specialKeys = keys.match(regexp) || []
const regularKeys = keys.split(regexp)
for (let i = 0; i < Math.max(specialKeys.length, regularKeys.length); i++) {
if (i < regularKeys.length && regularKeys[i]) {
for (const key of regularKeys[i].split("")) {
await sendSingleKey(key)
}
}
if (i < specialKeys.length) {
await sendSpecialKey(specialKeys[i])
}
}
}