commit a57302b5b9949a03f83c3a65463dd6e6c8c86bf9 parent ac0005704cf66a24d3fd226269f4e921ed526719 Author: Florian Dold <dold@taler.net> Date: Fri, 4 Sep 2026 21:07:36 +0200 taler-harness: make browser dependencies opt-in Diffstat:
16 files changed, 252 insertions(+), 158 deletions(-)
diff --git a/Makefile b/Makefile @@ -139,10 +139,6 @@ install-tools: $(MAKE) -C packages/anastasis-cli install-nodeps $(MAKE) -C packages/taler-harness install-nodeps -.PHONY: install-selenium -install-selenium: - $(MAKE) -C packages/taler-harness install-selenium - .PHONY: gana gana: ./contrib/gana_update.sh diff --git a/packages/taler-harness/Makefile b/packages/taler-harness/Makefile @@ -24,7 +24,7 @@ else BINDIR = $(prefix)/bin LIBDIR = $(prefix)/lib/taler-harness NODEDIR = $(LIBDIR)/node_modules/taler-harness -.PHONY: install deps install-nodeps install-selenium +.PHONY: install deps install-nodeps install-nodeps: install -d $(DESTDIR)$(BINDIR) install -d $(DESTDIR)$(NODEDIR) @@ -33,19 +33,8 @@ install-nodeps: install ./dist/taler-harness-bundled.cjs $(DESTDIR)$(NODEDIR)/dist/ install ./dist/taler-harness-bundled.cjs.map $(DESTDIR)$(NODEDIR)/dist/ install ./bin/taler-harness.mjs $(DESTDIR)$(NODEDIR)/bin/ -# Browser automation libraries cannot be bundled because they locate browsers -# and driver helpers relative to their package directories. Deploy all -# production dependencies so their transitive dependencies and pnpm links are -# preserved as well. - set -e; \ - runtime_deps_dir=$$(mktemp -d); \ - trap 'rm -rf "$$runtime_deps_dir"' EXIT; \ - pnpm --filter @gnu-taler/taler-harness deploy --prod --legacy "$$runtime_deps_dir"; \ - rm -rf $(DESTDIR)$(NODEDIR)/node_modules; \ - cp -a "$$runtime_deps_dir"/node_modules $(DESTDIR)$(NODEDIR)/ + install ./README.md $(DESTDIR)$(NODEDIR)/ ln -sf ../lib/taler-harness/node_modules/taler-harness/bin/taler-harness.mjs $(DESTDIR)$(BINDIR)/taler-harness -install-selenium: - npm install --prefix $(DESTDIR)$(NODEDIR) --no-save --omit=dev --ignore-scripts selenium-webdriver@4.40.0 deps: pnpm install --frozen-lockfile --filter @gnu-taler/taler-harness... pnpm run --filter @gnu-taler/taler-harness... build diff --git a/packages/taler-harness/README.md b/packages/taler-harness/README.md @@ -159,6 +159,25 @@ worker, while the crash or timeout already makes the test fail. ## Headless Web Integration test +### Browser test dependencies + +Playwright and Selenium are optional dependencies. They are available after a +normal `pnpm install` in the taler-typescript-core checkout, but are not copied +by `make install` because most harness commands and integration tests do not +need them. + +For an installed harness, install the library needed by the test into the +harness package directory: + +``` +npm install --prefix "<installation-prefix>/lib/taler-harness/node_modules/taler-harness" --no-save playwright-core@1.62.0 +npm install --prefix "<installation-prefix>/lib/taler-harness/node_modules/taler-harness" --no-save selenium-webdriver@4.40.0 +``` + +Replace `<installation-prefix>` with the prefix passed to `configure`. These +commands work independently of the current directory. A browser test reports +the applicable command when its Node.js dependency is missing. + 1) First you need the browsers that you are going to use to test Puppeteer scripts can be used to download diff --git a/packages/taler-harness/src/harness/bank-webui-browser.ts b/packages/taler-harness/src/harness/bank-webui-browser.ts @@ -18,6 +18,7 @@ import fs from "node:fs"; import path from "node:path"; import type { Browser, Page } from "playwright-core"; import { findBrowserBinary, installNativeUrl } from "../stagefright/stage.js"; +import { loadPlaywright } from "./browser-dependencies.js"; export type BankWebuiBrowserEngine = "chromium" | "firefox"; @@ -33,16 +34,6 @@ export interface BankWebuiBrowser { close: () => Promise<void>; } -async function loadPlaywright(): Promise<typeof import("playwright-core")> { - try { - return await import("playwright-core"); - } catch (cause) { - throw Error( - `unable to load playwright-core, run 'pnpm install' in taler-harness (${cause})`, - ); - } -} - /** Remove authentication material before browser diagnostics reach CI logs. */ export function sanitizeBankWebuiLog(text: string): string { return text diff --git a/packages/taler-harness/src/harness/browser-dependencies.ts b/packages/taler-harness/src/harness/browser-dependencies.ts @@ -0,0 +1,100 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. +*/ + +const PLAYWRIGHT_PACKAGE = "playwright-core@1.62.0"; +const SELENIUM_PACKAGE = "selenium-webdriver@4.40.0"; + +function missingBrowserDependency( + packageName: string, + packageSpec: string, + cause: unknown, +): Error { + return new Error( + `Unable to load ${packageName}. This browser command or integration test ` + + "requires the package to be installed next to taler-harness. From the " + + "taler-typescript-core checkout, run `pnpm install`. For an installed " + + "harness, run " + + `\`npm install --prefix <taler-harness-package-directory> --no-save ${packageSpec}\`. ` + + "See the 'Browser test dependencies' section in the taler-harness " + + "README.md.", + { cause }, + ); +} + +export async function loadPlaywright(): Promise< + typeof import("playwright-core") +> { + try { + return await import("playwright-core"); + } catch (cause) { + throw missingBrowserDependency( + "playwright-core", + PLAYWRIGHT_PACKAGE, + cause, + ); + } +} + +export async function loadSelenium(): Promise< + typeof import("selenium-webdriver") +> { + try { + return await import("selenium-webdriver"); + } catch (cause) { + throw missingBrowserDependency( + "selenium-webdriver", + SELENIUM_PACKAGE, + cause, + ); + } +} + +export async function loadSeleniumFirefox(): Promise< + typeof import("selenium-webdriver/firefox.js") +> { + try { + return await import("selenium-webdriver/firefox.js"); + } catch (cause) { + throw missingBrowserDependency( + "selenium-webdriver", + SELENIUM_PACKAGE, + cause, + ); + } +} + +export async function loadSeleniumChrome(): Promise< + typeof import("selenium-webdriver/chrome.js") +> { + try { + return await import("selenium-webdriver/chrome.js"); + } catch (cause) { + throw missingBrowserDependency( + "selenium-webdriver", + SELENIUM_PACKAGE, + cause, + ); + } +} + +export async function loadSeleniumScriptManager(): Promise< + typeof import("selenium-webdriver/bidi/scriptManager.js") +> { + try { + const imported = await import("selenium-webdriver/bidi/scriptManager.js"); + return ((imported as any).default ?? + imported) as typeof import("selenium-webdriver/bidi/scriptManager.js"); + } catch (cause) { + throw missingBrowserDependency( + "selenium-webdriver", + SELENIUM_PACKAGE, + cause, + ); + } +} diff --git a/packages/taler-harness/src/harness/environments.ts b/packages/taler-harness/src/harness/environments.ts @@ -96,31 +96,14 @@ import { import * as fs from "node:fs"; import * as http from "node:http"; import type { ThenableWebDriver } from "selenium-webdriver"; +import { + loadSelenium, + loadSeleniumChrome, + loadSeleniumFirefox, +} from "./browser-dependencies.js"; const logger = new Logger("helpers.ts"); -function seleniumLoadError(error: unknown): Error { - return new Error( - "Unable to load selenium-webdriver. Browser integration tests require " + - "this module. From the taler-typescript-core checkout, run " + - "`pnpm install --filter @gnu-taler/taler-harness...` and rebuild the " + - "harness. For an installed harness, run `make install-selenium` after " + - "configuring with the " + - "same prefix, or install it directly below " + - "`$PREFIX/lib/taler-harness/node_modules/taler-harness` with npm. " + - "Use the same prefix that was passed to configure.", - { cause: error }, - ); -} - -export async function loadSelenium() { - try { - return await import("selenium-webdriver"); - } catch (e) { - throw seleniumLoadError(e); - } -} - /** * Improved version of the simple test environment, * with the daemonized wallet. @@ -339,12 +322,7 @@ export function createBrowser(t: GlobalTestState) { switch (type) { case "firefox": { - let Firefox; - try { - Firefox = await import("selenium-webdriver/firefox.js"); - } catch (e) { - throw seleniumLoadError(e); - } + const Firefox = await loadSeleniumFirefox(); const firefoxOpts = new Firefox.Options(); firefoxOpts.addArguments("--headless"); if (process.env.BROWSER_BINARY) { @@ -358,12 +336,7 @@ export function createBrowser(t: GlobalTestState) { } case "chrome": { - let Chrome; - try { - Chrome = await import("selenium-webdriver/chrome.js"); - } catch (e) { - throw seleniumLoadError(e); - } + const Chrome = await loadSeleniumChrome(); const chromeOpts = new Chrome.Options(); // https://peter.sh/experiments/chromium-command-line-switches chromeOpts.addArguments("--no-sandbox", "-headless"); diff --git a/packages/taler-harness/src/harness/merchant-webui-browser.ts b/packages/taler-harness/src/harness/merchant-webui-browser.ts @@ -15,18 +15,9 @@ */ import { findBrowserBinary, installNativeUrl } from "../stagefright/stage.js"; +import { loadPlaywright } from "./browser-dependencies.js"; export { assertNoUnexpectedErrorBanner } from "./browser-assertions.js"; -async function loadPlaywright(): Promise<typeof import("playwright-core")> { - try { - return await import("playwright-core"); - } catch (e) { - throw Error( - `unable to load playwright-core, please run 'pnpm install' in the taler-harness package (${e})`, - ); - } -} - export interface MerchantWebuiBrowser { browser: any; page: any; diff --git a/packages/taler-harness/src/harness/webui-browser.ts b/packages/taler-harness/src/harness/webui-browser.ts @@ -8,14 +8,7 @@ */ import { findBrowserBinary, installNativeUrl } from "../stagefright/stage.js"; - -async function loadPlaywright(): Promise<typeof import("playwright-core")> { - try { - return await import("playwright-core"); - } catch (cause) { - throw Error(`unable to load playwright-core (${cause})`); - } -} +import { loadPlaywright } from "./browser-dependencies.js"; export interface WebuiBrowser { browser: any; diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-chrome-extension-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-chrome-extension-withdrawal.ts @@ -20,6 +20,7 @@ import net from "node:net"; import path from "node:path"; import * as nodeUrl from "node:url"; import type { BrowserContext, Page } from "playwright-core"; +import { loadPlaywright } from "../harness/browser-dependencies.js"; import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; import { GlobalTestState } from "../harness/harness.js"; import { findBrowserBinary } from "../stagefright/stage.js"; @@ -131,7 +132,7 @@ export async function runWalletWebUiChromeExtensionWithdrawalTest( "wallet-webui-extension-screenshots", ); fs.mkdirSync(screenshotDirectory, { recursive: true }); - const playwright = await import("playwright-core"); + const playwright = await loadPlaywright(); const savedUrl = globalThis.URL; const savedUrlSearchParams = globalThis.URLSearchParams; Object.assign(globalThis, { diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-extension-integration.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-extension-integration.ts @@ -13,8 +13,11 @@ import net from "node:net"; import path from "node:path"; import * as nodeUrl from "node:url"; import type { BrowserContext, Page } from "playwright-core"; -import { Builder, By, until } from "selenium-webdriver"; -import * as Firefox from "selenium-webdriver/firefox.js"; +import { + loadPlaywright, + loadSelenium, + loadSeleniumFirefox, +} from "../harness/browser-dependencies.js"; import { GlobalTestState } from "../harness/harness.js"; import { findBrowserBinary } from "../stagefright/stage.js"; import { @@ -26,6 +29,9 @@ const SETTINGS_KEY = "gnu-taler-wallet-browser-integration-v1"; const EXTENSION_ID = "wallet@taler.net"; const EXTENSION_UUID = "5e0d7cc0-49c7-4d18-8433-7b3c3e39de4d"; const TALER_URI = "taler://withdraw/bank.example/operation-123"; +type FirefoxDriver = import("selenium-webdriver/firefox.js").Driver; +let selenium: typeof import("selenium-webdriver"); +let Firefox: typeof import("selenium-webdriver/firefox.js"); interface IntegrationSettings { autoOpen: boolean; @@ -507,7 +513,7 @@ export async function runWalletWebUiChromeExtensionIntegrationTest( "default", "extension/chrome", ); - const playwright = await import("playwright-core"); + const playwright = await loadPlaywright(); const savedUrl = globalThis.URL; const savedUrlSearchParams = globalThis.URLSearchParams; Object.assign(globalThis, { @@ -553,7 +559,7 @@ export async function runWalletWebUiChromeExtensionIntegrationTest( } async function firefoxSetSettings( - driver: Firefox.Driver, + driver: FirefoxDriver, settings: IntegrationSettings, ): Promise<void> { const stored = await driver.executeAsyncScript( @@ -565,7 +571,7 @@ async function firefoxSetSettings( } async function firefoxPopupActionForPage( - driver: Firefox.Driver, + driver: FirefoxDriver, pageUrl: string, operation: "inspect" | "open", pageName = "taler-integration-current-page", @@ -578,7 +584,7 @@ async function firefoxPopupActionForPage( )) as { available?: boolean; opened?: boolean }; } -async function openFirefoxExtensionPage(driver: Firefox.Driver): Promise<void> { +async function openFirefoxExtensionPage(driver: FirefoxDriver): Promise<void> { await driver.setContext(Firefox.Context.CHROME); await driver.executeScript( "window.openTrustedLinkIn(arguments[0], 'current')", @@ -586,14 +592,14 @@ async function openFirefoxExtensionPage(driver: Firefox.Driver): Promise<void> { ); await driver.setContext(Firefox.Context.CONTENT); await driver.wait( - until.urlContains(`/wallet.html`), + selenium.until.urlContains(`/wallet.html`), 15_000, "Firefox extension page did not load", ); } async function firefoxValue<T>( - driver: Firefox.Driver, + driver: FirefoxDriver, script: string, ): Promise<T> { return (await driver.executeScript(script)) as T; @@ -603,6 +609,10 @@ async function firefoxValue<T>( export async function runWalletWebUiFirefoxExtensionIntegrationTest( t: GlobalTestState, ) { + [selenium, Firefox] = await Promise.all([ + loadSelenium(), + loadSeleniumFirefox(), + ]); const walletDirectory = findWalletWebUiDirectory(); await startBuild(t, walletDirectory); const fixture = await startFixtureServer(); @@ -621,11 +631,11 @@ export async function runWalletWebUiFirefoxExtensionIntegrationTest( const service = new Firefox.ServiceBuilder().addArguments( "--allow-system-access", ); - const driver = (await new Builder() + const driver = (await new selenium.Builder() .forBrowser("firefox") .setFirefoxOptions(options) .setFirefoxService(service) - .build()) as Firefox.Driver; + .build()) as FirefoxDriver; try { await driver.manage().setTimeouts({ pageLoad: 30_000, script: 30_000 }); const addonId = await driver.installAddon(extensionDirectory, true); @@ -697,9 +707,11 @@ export async function runWalletWebUiFirefoxExtensionIntegrationTest( await driver.executeScript( "document.querySelector('#taler-link').addEventListener('click', window.taler.__internal.anchorOnClick)", ); - await driver.findElement(By.id("taler-link")).click(); + await driver.findElement(selenium.By.id("taler-link")).click(); await driver.wait( - until.urlContains(`moz-extension://${EXTENSION_UUID}/wallet.html`), + selenium.until.urlContains( + `moz-extension://${EXTENSION_UUID}/wallet.html`, + ), 15_000, "Firefox compatibility API URL did not open the wallet", ); @@ -808,9 +820,11 @@ export async function runWalletWebUiFirefoxExtensionIntegrationTest( await driver.get(`${fixture.baseUrl}/?support=uri`); const beforeLink = await driver.getAllWindowHandles(); - await driver.findElement(By.id("taler-link")).click(); + await driver.findElement(selenium.By.id("taler-link")).click(); await driver.wait( - until.urlContains(`moz-extension://${EXTENSION_UUID}/wallet.html`), + selenium.until.urlContains( + `moz-extension://${EXTENSION_UUID}/wallet.html`, + ), 15_000, "Firefox URI link did not replace its source tab", ); @@ -822,7 +836,7 @@ export async function runWalletWebUiFirefoxExtensionIntegrationTest( await driver.get(`${fixture.baseUrl}/?support=uri&linkTarget=blank`); const beforeBlankLink = await driver.getAllWindowHandles(); - await driver.findElement(By.id("taler-link")).click(); + await driver.findElement(selenium.By.id("taler-link")).click(); await driver.wait( async () => (await driver.getAllWindowHandles()).length > beforeBlankLink.length, @@ -843,8 +857,11 @@ export async function runWalletWebUiFirefoxExtensionIntegrationTest( await openFirefoxExtensionPage(driver); await firefoxSetSettings(driver, { ...settings, hijackLinks: false }); await driver.get(`${fixture.baseUrl}/?support=uri&case=disabled`); - await driver.findElement(By.id("taler-link")).click(); - await driver.wait(until.elementLocated(By.id("taler-link")), 5_000); + await driver.findElement(selenium.By.id("taler-link")).click(); + await driver.wait( + selenium.until.elementLocated(selenium.By.id("taler-link")), + 5_000, + ); assert.equal(await firefoxValue(driver, "return window.fallbackCount"), 1); } finally { await driver.quit(); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-extension-sqlite.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-extension-sqlite.ts @@ -13,8 +13,11 @@ import fs from "node:fs"; import path from "node:path"; import * as nodeUrl from "node:url"; import type { BrowserContext, Page } from "playwright-core"; -import { Builder } from "selenium-webdriver"; -import * as Firefox from "selenium-webdriver/firefox.js"; +import { + loadPlaywright, + loadSelenium, + loadSeleniumFirefox, +} from "../harness/browser-dependencies.js"; import { GlobalTestState } from "../harness/harness.js"; import { findBrowserBinary } from "../stagefright/stage.js"; import { @@ -24,6 +27,9 @@ import { const FIREFOX_EXTENSION_ID = "wallet@taler.net"; const FIREFOX_EXTENSION_UUID = "5e0d7cc0-49c7-4d18-8433-7b3c3e39de4d"; +type FirefoxDriver = import("selenium-webdriver/firefox.js").Driver; +let selenium: typeof import("selenium-webdriver"); +let Firefox: typeof import("selenium-webdriver/firefox.js"); type SqliteTestMessage = | { @@ -225,7 +231,7 @@ export async function runWalletWebUiChromeExtensionSqliteTest( /wasm-unsafe-eval/, ); - const playwright = await import("playwright-core"); + const playwright = await loadPlaywright(); const savedUrl = globalThis.URL; const savedUrlSearchParams = globalThis.URLSearchParams; Object.assign(globalThis, { @@ -307,7 +313,7 @@ export async function runWalletWebUiChromeExtensionSqliteTest( } async function sendFromFirefox( - driver: Firefox.Driver, + driver: FirefoxDriver, message: SqliteTestMessage, ): Promise<SqliteTestResponse> { const envelope = (await driver.executeAsyncScript( @@ -328,6 +334,10 @@ async function sendFromFirefox( export async function runWalletWebUiFirefoxExtensionSqliteTest( t: GlobalTestState, ): Promise<void> { + [selenium, Firefox] = await Promise.all([ + loadSelenium(), + loadSeleniumFirefox(), + ]); const walletWebUiDirectory = findWalletWebUiDirectory(); const extensionDirectory = walletWebUiArtifactPath( walletWebUiDirectory, @@ -354,11 +364,11 @@ export async function runWalletWebUiFirefoxExtensionSqliteTest( const service = new Firefox.ServiceBuilder().addArguments( "--allow-system-access", ); - const driver = (await new Builder() + const driver = (await new selenium.Builder() .forBrowser("firefox") .setFirefoxOptions(options) .setFirefoxService(service) - .build()) as Firefox.Driver; + .build()) as FirefoxDriver; try { await driver .manage() diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-extension-upgrade.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-extension-upgrade.ts @@ -13,8 +13,11 @@ import fs from "node:fs"; import path from "node:path"; import * as nodeUrl from "node:url"; import type { BrowserContext, Page } from "playwright-core"; -import { Builder } from "selenium-webdriver"; -import * as Firefox from "selenium-webdriver/firefox.js"; +import { + loadPlaywright, + loadSelenium, + loadSeleniumFirefox, +} from "../harness/browser-dependencies.js"; import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; import { GlobalTestState } from "../harness/harness.js"; import { findBrowserBinary } from "../stagefright/stage.js"; @@ -25,6 +28,10 @@ import { const FIREFOX_EXTENSION_ID = "wallet@taler.net"; const FIREFOX_EXTENSION_UUID = "acdb6f48-4e4c-4f3f-a3bc-987b23cd1a57"; +type FirefoxDriver = import("selenium-webdriver/firefox.js").Driver; +type FirefoxOptions = import("selenium-webdriver/firefox.js").Options; +let selenium: typeof import("selenium-webdriver"); +let Firefox: typeof import("selenium-webdriver/firefox.js"); interface UpgradeArtifacts { legacy: string; @@ -272,7 +279,7 @@ async function confirmBankWithdrawal( } async function seedLegacyFirefoxWallet( - driver: Firefox.Driver, + driver: FirefoxDriver, talerWithdrawUri: string, exchangeBaseUrl: string, ): Promise<void> { @@ -319,7 +326,7 @@ async function launchChrome( profile: string, extension: string, ): Promise<BrowserContext> { - const playwright = await import("playwright-core"); + const playwright = await loadPlaywright(); return playwright.chromium.launchPersistentContext(profile, { headless: true, executablePath: findBrowserBinary(), @@ -428,7 +435,7 @@ export async function runWalletWebUiChromeExtensionUpgradeTest( runWalletWebUiChromeExtensionUpgradeTest.suites = ["wallet-webui"]; runWalletWebUiChromeExtensionUpgradeTest.timeoutMs = 300_000; -function firefoxOptions(profile: string): Firefox.Options { +function firefoxOptions(profile: string): FirefoxOptions { const options = new Firefox.Options() .addArguments("-headless", "-profile", profile) .setPreference( @@ -439,19 +446,19 @@ function firefoxOptions(profile: string): Firefox.Options { return options; } -async function launchFirefox(profile: string): Promise<Firefox.Driver> { +async function launchFirefox(profile: string): Promise<FirefoxDriver> { const service = new Firefox.ServiceBuilder().addArguments( "--allow-system-access", ); - return (await new Builder() + return (await new selenium.Builder() .forBrowser("firefox") .setFirefoxOptions(firefoxOptions(profile)) .setFirefoxService(service) - .build()) as Firefox.Driver; + .build()) as FirefoxDriver; } async function openFirefoxExtensionPage( - driver: Firefox.Driver, + driver: FirefoxDriver, pathName: string, ): Promise<void> { await driver.setContext(Firefox.Context.CHROME); @@ -470,7 +477,7 @@ async function openFirefoxExtensionPage( } async function firefoxCall( - driver: Firefox.Driver, + driver: FirefoxDriver, protocol: "legacy" | "wallet-webui", operation: string, args: unknown, @@ -521,6 +528,10 @@ async function firefoxCall( export async function runWalletWebUiFirefoxExtensionUpgradeTest( t: GlobalTestState, ) { + [selenium, Firefox] = await Promise.all([ + loadSelenium(), + loadSeleniumFirefox(), + ]); const { bankClient, exchange } = await createSimpleTestkudosEnvironmentV3(t); const { user, withdrawal } = await createBankWithdrawal(bankClient); const artifacts = await prepareArtifacts(t, "firefox"); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-firefox-extension-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-firefox-extension-withdrawal.ts @@ -18,8 +18,10 @@ import fs from "node:fs"; import http from "node:http"; import net from "node:net"; import path from "node:path"; -import { BrowsingContext, Builder, By, until } from "selenium-webdriver"; -import * as Firefox from "selenium-webdriver/firefox.js"; +import { + loadSelenium, + loadSeleniumFirefox, +} from "../harness/browser-dependencies.js"; import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; import { GlobalTestState } from "../harness/harness.js"; import { @@ -29,6 +31,9 @@ import { const EXTENSION_ID = "wallet@taler.net"; const EXTENSION_UUID = "5e0d7cc0-49c7-4d18-8433-7b3c3e39de4d"; +type FirefoxDriver = import("selenium-webdriver/firefox.js").Driver; +let selenium: typeof import("selenium-webdriver"); +let Firefox: typeof import("selenium-webdriver/firefox.js"); async function reserveTcpPort(): Promise<number> { const server = net.createServer(); @@ -68,7 +73,7 @@ async function waitForBuild(url: string, timeoutMs = 90_000): Promise<void> { } async function screenshot( - driver: Firefox.Driver, + driver: FirefoxDriver, directory: string, name: string, ): Promise<void> { @@ -79,19 +84,16 @@ async function screenshot( ); } -async function waitForXpath(driver: Firefox.Driver, xpath: string) { +async function waitForXpath(driver: FirefoxDriver, xpath: string) { const element = await driver.wait( - until.elementLocated(By.xpath(xpath)), + selenium.until.elementLocated(selenium.By.xpath(xpath)), 30_000, ); - await driver.wait(until.elementIsVisible(element), 30_000); + await driver.wait(selenium.until.elementIsVisible(element), 30_000); return element; } -async function clickXpath( - driver: Firefox.Driver, - xpath: string, -): Promise<void> { +async function clickXpath(driver: FirefoxDriver, xpath: string): Promise<void> { const element = await waitForXpath(driver, xpath); await driver.executeScript( "arguments[0].scrollIntoView({ block: 'center', inline: 'nearest' })", @@ -101,7 +103,7 @@ async function clickXpath( } async function setWalletRoute( - driver: Firefox.Driver, + driver: FirefoxDriver, route: string, ): Promise<void> { await driver.executeScript( @@ -114,6 +116,10 @@ async function setWalletRoute( export async function runWalletWebUiFirefoxExtensionWithdrawalTest( t: GlobalTestState, ) { + [selenium, Firefox] = await Promise.all([ + loadSelenium(), + loadSeleniumFirefox(), + ]); const { exchange } = await createSimpleTestkudosEnvironmentV3(t); const walletWebUiDirectory = findWalletWebUiDirectory(); const buildPort = await reserveTcpPort(); @@ -146,11 +152,11 @@ export async function runWalletWebUiFirefoxExtensionWithdrawalTest( const service = new Firefox.ServiceBuilder().addArguments( "--allow-system-access", ); - const driver = (await new Builder() + const driver = (await new selenium.Builder() .forBrowser("firefox") .setFirefoxOptions(options) .setFirefoxService(service) - .build()) as Firefox.Driver; + .build()) as FirefoxDriver; try { await driver @@ -189,7 +195,9 @@ export async function runWalletWebUiFirefoxExtensionWithdrawalTest( "//*[normalize-space(.)='Welcome to Taler Wallet!']", ); const contexts = await ( - await BrowsingContext(driver, { browsingContextId: walletHandle }) + await selenium.BrowsingContext(driver, { + browsingContextId: walletHandle, + }) ).getTopLevelContexts(); if (!contexts.some((context) => context.url.includes("/wallet.html"))) { throw Error("WebDriver BiDi did not observe the full wallet context"); @@ -198,8 +206,8 @@ export async function runWalletWebUiFirefoxExtensionWithdrawalTest( await setWalletRoute(driver, "/exchanges"); await waitForXpath(driver, "//h1[normalize-space(.)='Exchanges']"); const exchangeInput = await driver.wait( - until.elementLocated( - By.css('input[placeholder="https://exchange.example/"]'), + selenium.until.elementLocated( + selenium.By.css('input[placeholder="https://exchange.example/"]'), ), 30_000, ); @@ -217,7 +225,9 @@ export async function runWalletWebUiFirefoxExtensionWithdrawalTest( await setWalletRoute(driver, "/withdraw?kind=demo"); await waitForXpath(driver, "//h1[normalize-space(.)='Withdraw']"); const amountInput = await driver.wait( - until.elementLocated(By.css('input[inputmode="decimal"]')), + selenium.until.elementLocated( + selenium.By.css('input[inputmode="decimal"]'), + ), 30_000, ); await amountInput.sendKeys("10"); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-startup.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-startup.ts @@ -17,6 +17,7 @@ import path from "node:path"; import * as nodeUrl from "node:url"; import type { Page } from "playwright-core"; +import { loadPlaywright } from "../harness/browser-dependencies.js"; import { GlobalTestState } from "../harness/harness.js"; import { findBrowserBinary } from "../stagefright/stage.js"; import { @@ -100,7 +101,7 @@ export async function runWalletWebUiPwaStartupTest(t: GlobalTestState) { ); await waitForPwa(pwaUrl); - const playwright = await import("playwright-core"); + const playwright = await loadPlaywright(); const profileDirectory = path.join(t.testDir, "wallet-webui-pwa-profile"); const savedUrl = globalThis.URL; const savedUrlSearchParams = globalThis.URLSearchParams; diff --git a/packages/taler-harness/src/stagefright/stage.ts b/packages/taler-harness/src/stagefright/stage.ts @@ -31,6 +31,7 @@ import os from "node:os"; import path from "node:path"; import * as nodeUrl from "node:url"; import type { Browser, BrowserContext, Page } from "playwright-core"; +import { loadPlaywright } from "../harness/browser-dependencies.js"; import { assertNoUnexpectedErrorBanner } from "../harness/browser-assertions.js"; const logger = new Logger("stagefright/stage.ts"); @@ -168,21 +169,6 @@ export function installNativeUrl(): () => void { }; } -/** - * Load playwright lazily: the harness is bundled as a single file and - * playwright is kept external, so a missing installation must not break - * unrelated subcommands. - */ -async function loadPlaywright(): Promise<typeof import("playwright-core")> { - try { - return await import("playwright-core"); - } catch (e) { - throw Error( - `unable to load playwright-core, please run 'pnpm install' in the taler-harness package (${e})`, - ); - } -} - export class Stage { private stepCounter = 0; private failureDumped = false; diff --git a/packages/taler-harness/src/stagefright/webdriver-stage.ts b/packages/taler-harness/src/stagefright/webdriver-stage.ts @@ -20,21 +20,22 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { Page } from "playwright-core"; +import type { WebDriver, WebElement } from "selenium-webdriver"; import { - Builder, - Key, - until, - type WebDriver, - type WebElement, -} from "selenium-webdriver"; -import ScriptManager from "selenium-webdriver/bidi/scriptManager.js"; -import * as Firefox from "selenium-webdriver/firefox.js"; + loadSelenium, + loadSeleniumFirefox, + loadSeleniumScriptManager, +} from "../harness/browser-dependencies.js"; import { DEFAULT_TIMEOUT_MS, defaultScreenshotDir, type StageOptions, } from "./stage.js"; +let selenium: typeof import("selenium-webdriver"); +let Firefox: typeof import("selenium-webdriver/firefox.js"); +let ScriptManager: typeof import("selenium-webdriver/bidi/scriptManager.js"); + type TextMatcher = string | RegExp; interface LocatorOptions { @@ -251,7 +252,7 @@ class WebDriverLocator { const element = await this.one(options.timeout); if (options.state !== "attached") { await this.page.driver.wait( - until.elementIsVisible(element), + selenium.until.elementIsVisible(element), options.timeout ?? this.page.timeoutMs, ); } @@ -264,7 +265,7 @@ class WebDriverLocator { element, ); await this.page.driver.wait( - until.elementIsVisible(element), + selenium.until.elementIsVisible(element), this.page.timeoutMs, ); await element.click(); @@ -289,7 +290,7 @@ class WebDriverLocator { async press(key: string): Promise<void> { const element = await this.one(); - await element.sendKeys(key === "Enter" ? Key.ENTER : key); + await element.sendKeys(key === "Enter" ? selenium.Key.ENTER : key); } async selectOption(value: string): Promise<void> { @@ -541,6 +542,11 @@ export class WebDriverStage { if (options.browserType && options.browserType !== "firefox") { throw Error("the WebDriver stage currently supports Firefox only"); } + [selenium, Firefox, ScriptManager] = await Promise.all([ + loadSelenium(), + loadSeleniumFirefox(), + loadSeleniumScriptManager(), + ]); const screenshotDir = options.screenshotDir ?? defaultScreenshotDir(scenarioName); const downloadDir = fs.mkdtempSync( @@ -566,7 +572,7 @@ export class WebDriverStage { const service = new Firefox.ServiceBuilder().addArguments( "--allow-system-access", ); - const driver = await new Builder() + const driver = await new selenium.Builder() .forBrowser("firefox") .setFirefoxOptions(firefoxOptions) .setFirefoxService(service)