taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit fa7459c710ba8bc13bc1fa15469f104fdd5fad86
parent 0c81cdba8ed3291374a848cab7d765cfa69a77a7
Author: Florian Dold <dold@taler.net>
Date:   Fri, 28 Aug 2026 14:36:52 +0200

taler-harness: run WebUI tests from installed artifacts

Diffstat:
MMakefile | 2++
Mpackages/libeufin-bank-webui/src/app.tsx | 30++++++++++++++++++------------
Mpackages/taler-harness/build.mjs | 29++++++++++++++++++++++++-----
Mpackages/taler-harness/src/harness/webui-server.ts | 46+++++++++++++++++++++++++++++++++++++---------
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-chrome-extension-withdrawal.ts | 25++++++++-----------------
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-demo.ts | 10++++++++--
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-extension-integration.ts | 28+++++++++++-----------------
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-extension-sqlite.ts | 29+++++++++++------------------
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-extension-upgrade.ts | 43++++++++++++++++---------------------------
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-firefox-extension-withdrawal.ts | 25++++++++-----------------
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-cash-acceptor.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-deposit-kyc-auth-swiss.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-deposit-kyc-auth.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-donau.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-errors.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-lifecycle.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-deposit.ts | 10++++++++--
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-tos.ts | 7+++++--
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-sqlite.ts | 32+++++++++++++++-----------------
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-startup.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-storybook.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-withdrawal.ts | 55++++++++++++++++++++-----------------------------------
Mpackages/taler-harness/src/integrationtests/wallet-web-ui-test-support.ts | 46++++++++++++++++++++++++++++++++++++++++------
Apackages/wallet-webui/Makefile | 31+++++++++++++++++++++++++++++++
Apackages/wallet-webui/build-harness.mjs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/wallet-webui/package.json | 3++-
Apackages/wallet-webui/test-server.mjs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
27 files changed, 418 insertions(+), 195 deletions(-)

diff --git a/Makefile b/Makefile @@ -99,9 +99,11 @@ ast-grep: install: pnpm install --frozen-lockfile --filter '!@gnu-taler/qa-tooling' pnpm run build + pnpm run --filter @gnu-taler/wallet-webui build:harness $(MAKE) -C packages/taler-wallet-cli install-nodeps $(MAKE) -C packages/anastasis-cli install-nodeps $(MAKE) -C packages/taler-harness install-nodeps + $(MAKE) -C packages/wallet-webui install-nodeps $(MAKE) -C packages/libeufin-bank-webui install-nodeps $(MAKE) -C packages/taler-merchant-webui install-nodeps $(MAKE) -C packages/taler-auditor-webui install-nodeps diff --git a/packages/libeufin-bank-webui/src/app.tsx b/packages/libeufin-bank-webui/src/app.tsx @@ -66,6 +66,14 @@ import { DeveloperSettings } from "./pages/DeveloperSettings.js"; const WITH_LOCAL_STORAGE_CACHE = false; export function App() { + return ( + <TranslationProvider source={strings}> + <ConfiguredApp /> + </TranslationProvider> + ); +} + +function ConfiguredApp() { const [settings, setSettings] = useState<UiSettings>(); const [developerOverrides, setDeveloperOverrides] = useState<DeveloperOverrides>(() => @@ -101,18 +109,16 @@ export function App() { return ( <SettingsProvider value={effectiveSettings}> - <TranslationProvider source={strings}> - <NotificationProvider> - <BrowserHashNavigationProvider> - <SubApp - baseUrl={baseUrl} - settings={settings} - developerOverrides={developerOverrides} - onUpdateDeveloperOverrides={updateDeveloperOverrides} - /> - </BrowserHashNavigationProvider> - </NotificationProvider> - </TranslationProvider> + <NotificationProvider> + <BrowserHashNavigationProvider> + <SubApp + baseUrl={baseUrl} + settings={settings} + developerOverrides={developerOverrides} + onUpdateDeveloperOverrides={updateDeveloperOverrides} + /> + </BrowserHashNavigationProvider> + </NotificationProvider> </SettingsProvider> ); } diff --git a/packages/taler-harness/build.mjs b/packages/taler-harness/build.mjs @@ -18,7 +18,7 @@ import esbuild from "esbuild"; import fs from "node:fs"; import path from "node:path"; -import { execSync } from 'node:child_process'; +import { execSync } from "node:child_process"; const BASE = process.cwd(); @@ -27,19 +27,37 @@ while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { GIT_ROOT = path.join(GIT_ROOT, "../"); } -const GIT_HASH = GIT_ROOT === "/" ? 'not defined' : git_hash(); +const GIT_HASH = GIT_ROOT === "/" ? "not defined" : git_hash(); +const INSTALL_PREFIX = get_install_prefix(); -let PACKAGE_VERSION = get_version() +let PACKAGE_VERSION = get_version(); function get_version() { try { return JSON.parse(fs.readFileSync(path.join(BASE, "package.json"))).version; } catch { - return 'not defined' + return "not defined"; } } function git_hash() { - return execSync(`git rev-parse HEAD`, { encoding: 'utf-8' }).trim(); + return execSync(`git rev-parse HEAD`, { encoding: "utf-8" }).trim(); +} + +function get_install_prefix() { + const candidates = [ + path.join(BASE, ".config.mk"), + path.join(BASE, "../../.config.mk"), + ]; + for (const filename of candidates) { + try { + const config = fs.readFileSync(filename, "utf8"); + const match = /^prefix\s*=\s*(.*?)\s*$/m.exec(config); + if (match?.[1]) return match[1]; + } catch { + // Building without configure uses configure's default prefix. + } + } + return "/usr/local"; } // Still commonjs, because axios doesn't work properly under mjs @@ -59,6 +77,7 @@ export const buildConfig = { define: { __VERSION__: `"${PACKAGE_VERSION}"`, __GIT_HASH__: `"${GIT_HASH}"`, + __INSTALL_PREFIX__: JSON.stringify(INSTALL_PREFIX), "walletCoreBuildInfo.implementationSemver": `"${PACKAGE_VERSION}"`, "walletCoreBuildInfo.implementationGitHash": `"${GIT_HASH}"`, "import.meta.url": "import_meta_url", diff --git a/packages/taler-harness/src/harness/webui-server.ts b/packages/taler-harness/src/harness/webui-server.ts @@ -22,6 +22,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; const logger = new Logger("harness/webui-server.ts"); +declare const __INSTALL_PREFIX__: string; export interface WebuiServer { url: string; @@ -32,22 +33,51 @@ export interface WebuiServerOptions { experimental?: boolean; } -function findWorkspaceRoot(packageName: string): string { +function findWorkspacePackage( + packageName: string, +): { packageDir: string; rootDir: string } | undefined { let rootDir = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "../../..", ); if (fs.existsSync(path.join(rootDir, `packages/${packageName}`))) { - return rootDir; + return { + rootDir, + packageDir: path.join(rootDir, `packages/${packageName}`), + }; } let curr = process.cwd(); while (curr !== path.parse(curr).root) { if (fs.existsSync(path.join(curr, `packages/${packageName}`))) { - return curr; + return { + rootDir: curr, + packageDir: path.join(curr, `packages/${packageName}`), + }; } curr = path.dirname(curr); } - throw new Error(`could not find packages/${packageName} in a workspace`); + return undefined; +} + +function findWebuiBuild(packageName: string): { + distDir: string; + packageDir?: string; + rootDir?: string; +} { + const installed = path.join(__INSTALL_PREFIX__, "share", packageName); + if (fs.existsSync(path.join(installed, "index.html"))) { + return { distDir: installed }; + } + const workspace = findWorkspacePackage(packageName); + if (workspace) { + return { + ...workspace, + distDir: path.join(workspace.packageDir, "dist/prod"), + }; + } + throw new Error( + `could not find installed ${packageName} WebUI below ${__INSTALL_PREFIX__}/share or in a workspace`, + ); } function contentTypeFor(filePath: string): string { @@ -85,17 +115,15 @@ async function startStaticWebuiServer(args: { dynamicFiles: Record<string, unknown | ((serverBaseUrl: string) => unknown)>; proxyBaseUrl?: string; }): Promise<WebuiServer> { - const rootDir = findWorkspaceRoot(args.packageName); - const packageDir = path.join(rootDir, `packages/${args.packageName}`); - const distDir = path.join(packageDir, "dist/prod"); - if (webuiBuildIsStale(packageDir, distDir)) { + const { rootDir, packageDir, distDir } = findWebuiBuild(args.packageName); + if (rootDir && packageDir && webuiBuildIsStale(packageDir, distDir)) { logger.info(`building ${args.packageName} package...`); execFileSync("pnpm", ["--filter", args.packageFilter, "build"], { cwd: rootDir, stdio: "inherit", }); } - if (!fs.existsSync(distDir)) { + if (!fs.existsSync(path.join(distDir, "index.html"))) { throw new Error(`${args.packageName} build did not create ${distDir}`); } 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 @@ -23,20 +23,10 @@ import type { BrowserContext, Page } from "playwright-core"; import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; import { GlobalTestState } from "../harness/harness.js"; import { findBrowserBinary } from "../stagefright/stage.js"; - -function findWalletWebUiDirectory(): string { - const candidates = [ - process.env.TALER_WALLET_WEBUI_DIR, - path.resolve(process.cwd(), "packages/wallet-webui"), - path.resolve(process.cwd(), "../wallet-webui"), - ].filter((candidate): candidate is string => Boolean(candidate)); - for (const candidate of candidates) { - if (fs.existsSync(path.join(candidate, "dev.mjs"))) return candidate; - } - throw Error( - "wallet-webui sources not found; run from the wallet-core checkout or set TALER_WALLET_WEBUI_DIR", - ); -} +import { + findWalletWebUiDirectory, + walletWebUiArtifactPath, +} from "./wallet-web-ui-test-support.js"; async function reserveTcpPort(): Promise<number> { const server = net.createServer(); @@ -126,14 +116,15 @@ export async function runWalletWebUiChromeExtensionWithdrawalTest( const buildPort = await reserveTcpPort(); t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${buildPort}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${buildPort}`], "wallet-webui-build", ); await waitForBuild(`http://127.0.0.1:${buildPort}/`); - const extensionDirectory = path.join( + const extensionDirectory = walletWebUiArtifactPath( walletWebUiDirectory, - "dist/extension/chrome", + "default", + "extension/chrome", ); const screenshotDirectory = path.join( t.testDir, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-demo.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-demo.ts @@ -23,7 +23,11 @@ export async function runWalletWebUiDemoTest(t: GlobalTestState) { const demoUrl = `http://127.0.0.1:${port}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), "--demo", `--port=${port}`], + [ + path.join(walletWebUiDirectory, "test-server.mjs"), + "--demo", + `--port=${port}`, + ], "wallet-webui-demo", ); await waitForPwa(demoUrl); @@ -132,7 +136,9 @@ export async function runWalletWebUiDemoTest(t: GlobalTestState) { await page.getByText("Payment options", { exact: true }).waitFor(); await page.getByRole("button", { name: "Pay CHF:5.10" }).click(); await page.getByText("Completed", { exact: true }).waitFor(); - await page.goto(demoUrl); + // Demo state is deliberately in-memory, so keep this navigation inside + // the SPA instead of reloading and constructing a fresh demo wallet. + await page.getByRole("link", { name: "Assets", exact: true }).click(); await page .locator("strong:visible", { hasText: "37.40" }) .first() 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 @@ -18,6 +18,10 @@ import { Builder, By, until } from "selenium-webdriver"; import * as Firefox from "selenium-webdriver/firefox.js"; import { GlobalTestState } from "../harness/harness.js"; import { findBrowserBinary } from "../stagefright/stage.js"; +import { + findWalletWebUiDirectory, + walletWebUiArtifactPath, +} from "./wallet-web-ui-test-support.js"; const SETTINGS_KEY = "gnu-taler-wallet-browser-integration-v1"; const EXTENSION_ID = "wallet@taler.net"; @@ -31,18 +35,6 @@ interface IntegrationSettings { hijackLinks: boolean; } -function findWalletWebUiDirectory(): string { - const candidates = [ - process.env.TALER_WALLET_WEBUI_DIR, - path.resolve(process.cwd(), "packages/wallet-webui"), - path.resolve(process.cwd(), "../wallet-webui"), - ].filter((candidate): candidate is string => Boolean(candidate)); - for (const candidate of candidates) { - if (fs.existsSync(path.join(candidate, "dev.mjs"))) return candidate; - } - throw Error("wallet-webui sources not found"); -} - async function reserveTcpPort(): Promise<number> { const server = net.createServer(); await new Promise<void>((resolve, reject) => { @@ -147,7 +139,7 @@ function startBuild( return reserveTcpPort().then(async (port) => { t.spawnService( process.execPath, - [path.join(walletDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-integration-build", ); await waitForBuild(`http://127.0.0.1:${port}/`); @@ -511,9 +503,10 @@ export async function runWalletWebUiChromeExtensionIntegrationTest( const walletDirectory = findWalletWebUiDirectory(); await startBuild(t, walletDirectory); const fixture = await startFixtureServer(); - const extensionDirectory = path.join( + const extensionDirectory = walletWebUiArtifactPath( walletDirectory, - "dist/extension/chrome", + "default", + "extension/chrome", ); const playwright = await import("playwright-core"); const savedUrl = globalThis.URL; @@ -614,9 +607,10 @@ export async function runWalletWebUiFirefoxExtensionIntegrationTest( const walletDirectory = findWalletWebUiDirectory(); await startBuild(t, walletDirectory); const fixture = await startFixtureServer(); - const extensionDirectory = path.join( + const extensionDirectory = walletWebUiArtifactPath( walletDirectory, - "dist/extension/firefox", + "default", + "extension/firefox", ); const options = new Firefox.Options() .addArguments("-headless") 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 @@ -15,9 +15,12 @@ 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 { GlobalTestState, runCommand } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { findBrowserBinary } from "../stagefright/stage.js"; -import { findWalletWebUiDirectory } from "./wallet-web-ui-test-support.js"; +import { + findWalletWebUiDirectory, + walletWebUiArtifactPath, +} from "./wallet-web-ui-test-support.js"; const FIREFOX_EXTENSION_ID = "wallet@taler.net"; const FIREFOX_EXTENSION_UUID = "5e0d7cc0-49c7-4d18-8433-7b3c3e39de4d"; @@ -39,16 +42,6 @@ type SqliteTestResponse = | { ok: true; host: string; result?: any } | { ok: false; host: string; error: string }; -async function startSqliteTestBuild( - t: GlobalTestState, - walletWebUiDirectory: string, -): Promise<void> { - await runCommand(t, "wallet-webui-extension-sqlite-build", process.execPath, [ - path.join(walletWebUiDirectory, "build.mjs"), - "--test-extension-sqlite", - ]); -} - function readManifest(directory: string): any { return JSON.parse( fs.readFileSync(path.join(directory, "manifest.json"), "utf8"), @@ -220,10 +213,10 @@ export async function runWalletWebUiChromeExtensionSqliteTest( t: GlobalTestState, ): Promise<void> { const walletWebUiDirectory = findWalletWebUiDirectory(); - await startSqliteTestBuild(t, walletWebUiDirectory); - const extensionDirectory = path.join( + const extensionDirectory = walletWebUiArtifactPath( walletWebUiDirectory, - "dist/extension/chrome", + "extension-sqlite", + "extension/chrome", ); const manifest = readManifest(extensionDirectory); assert.ok(manifest.permissions.includes("offscreen")); @@ -336,10 +329,10 @@ export async function runWalletWebUiFirefoxExtensionSqliteTest( t: GlobalTestState, ): Promise<void> { const walletWebUiDirectory = findWalletWebUiDirectory(); - await startSqliteTestBuild(t, walletWebUiDirectory); - const extensionDirectory = path.join( + const extensionDirectory = walletWebUiArtifactPath( walletWebUiDirectory, - "dist/extension/firefox", + "extension-sqlite", + "extension/firefox", ); const manifest = readManifest(extensionDirectory); assert.equal(manifest.permissions.includes("offscreen"), false); 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 @@ -16,9 +16,12 @@ import type { BrowserContext, Page } from "playwright-core"; import { Builder } from "selenium-webdriver"; import * as Firefox from "selenium-webdriver/firefox.js"; import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; -import { GlobalTestState, runCommand } from "../harness/harness.js"; +import { GlobalTestState } from "../harness/harness.js"; import { findBrowserBinary } from "../stagefright/stage.js"; -import { findWalletWebUiDirectory } from "./wallet-web-ui-test-support.js"; +import { + findWalletWebUiDirectory, + walletWebUiArtifactPath, +} from "./wallet-web-ui-test-support.js"; const FIREFOX_EXTENSION_ID = "wallet@taler.net"; const FIREFOX_EXTENSION_UUID = "acdb6f48-4e4c-4f3f-a3bc-987b23cd1a57"; @@ -82,7 +85,7 @@ function stageNextExtension( destination: string, ): string { fs.rmSync(destination, { recursive: true, force: true }); - fs.cpSync(path.join(source, "dist/extension", target), destination, { + fs.cpSync(path.join(source, target), destination, { recursive: true, }); const manifestFile = path.join(destination, "manifest.json"); @@ -105,33 +108,19 @@ async function prepareArtifacts( target: "chrome" | "firefox", ): Promise<UpgradeArtifacts> { const walletWebUi = findWalletWebUiDirectory(); - const repositoryRoot = path.resolve(walletWebUi, "../.."); - const backgroundBundle = path.join( - t.testDir, - `legacy-background-${target}.js`, - ); - await runCommand( - t, - `legacy-extension-build-${target}`, - path.join(repositoryRoot, "node_modules/.bin/esbuild"), - [ - path.join(walletWebUi, "src/testing/legacy-extension-background.ts"), - "--bundle", - "--platform=browser", - "--format=iife", - "--conditions=browser", - `--outfile=${backgroundBundle}`, - ], - ); - await runCommand( - t, - `wallet-webui-extension-build-${target}`, - process.execPath, - [path.join(walletWebUi, "build.mjs")], + const backgroundBundle = walletWebUiArtifactPath( + walletWebUi, + "legacy", + "background.js", ); const legacy = path.join(t.testDir, `legacy-extension-${target}`); const next = path.join(t.testDir, `wallet-webui-extension-${target}`); - const legacyVersion = stageNextExtension(walletWebUi, target, next); + const extensionRoot = walletWebUiArtifactPath( + walletWebUi, + "default", + "extension", + ); + const legacyVersion = stageNextExtension(extensionRoot, target, next); stageLegacyExtension(backgroundBundle, legacy, target, legacyVersion); if (target === "chrome") { const { publicKey } = generateKeyPairSync("rsa", { 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 @@ -22,24 +22,14 @@ import { BrowsingContext, Builder, By, until } from "selenium-webdriver"; import * as Firefox from "selenium-webdriver/firefox.js"; import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; import { GlobalTestState } from "../harness/harness.js"; +import { + findWalletWebUiDirectory, + walletWebUiArtifactPath, +} from "./wallet-web-ui-test-support.js"; const EXTENSION_ID = "wallet@taler.net"; const EXTENSION_UUID = "5e0d7cc0-49c7-4d18-8433-7b3c3e39de4d"; -function findWalletWebUiDirectory(): string { - const candidates = [ - process.env.TALER_WALLET_WEBUI_DIR, - path.resolve(process.cwd(), "packages/wallet-webui"), - path.resolve(process.cwd(), "../wallet-webui"), - ].filter((candidate): candidate is string => Boolean(candidate)); - for (const candidate of candidates) { - if (fs.existsSync(path.join(candidate, "dev.mjs"))) return candidate; - } - throw Error( - "wallet-webui sources not found; run from the wallet-core checkout or set TALER_WALLET_WEBUI_DIR", - ); -} - async function reserveTcpPort(): Promise<number> { const server = net.createServer(); await new Promise<void>((resolve, reject) => { @@ -129,14 +119,15 @@ export async function runWalletWebUiFirefoxExtensionWithdrawalTest( const buildPort = await reserveTcpPort(); t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${buildPort}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${buildPort}`], "wallet-webui-build", ); await waitForBuild(`http://127.0.0.1:${buildPort}/`); - const extensionDirectory = path.join( + const extensionDirectory = walletWebUiArtifactPath( walletWebUiDirectory, - "dist/extension/firefox", + "default", + "extension/firefox", ); const screenshotDirectory = path.join( t.testDir, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-cash-acceptor.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-cash-acceptor.ts @@ -49,7 +49,7 @@ export async function runWalletWebUiPwaCashAcceptorTest(t: GlobalTestState) { const pwaUrl = `http://127.0.0.1:${port}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-pwa-cash-acceptor", ); await waitForPwa(pwaUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-deposit-kyc-auth-swiss.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-deposit-kyc-auth-swiss.ts @@ -129,7 +129,7 @@ export async function runWalletWebUiPwaDepositKycAuthSwissTest( const pwaUrl = `http://127.0.0.1:${port}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-pwa-deposit-kyc-auth-swiss", ); await waitForPwa(pwaUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-deposit-kyc-auth.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-deposit-kyc-auth.ts @@ -87,7 +87,7 @@ export async function runWalletWebUiPwaDepositKycAuthTest(t: GlobalTestState) { const pwaUrl = `http://127.0.0.1:${port}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-pwa-deposit-kyc-auth", ); await waitForPwa(pwaUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-donau.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-donau.ts @@ -138,7 +138,7 @@ export async function runWalletWebUiPwaDonauTest(t: GlobalTestState) { const pwaUrl = `http://127.0.0.1:${pwaPort}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${pwaPort}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${pwaPort}`], "wallet-webui-pwa", ); await waitForPwa(pwaUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-errors.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-errors.ts @@ -52,7 +52,7 @@ export async function runWalletWebUiPwaErrorsTest(t: GlobalTestState) { const pwaUrl = `http://127.0.0.1:${port}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-pwa-errors", ); await waitForPwa(pwaUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-lifecycle.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-lifecycle.ts @@ -23,7 +23,7 @@ export async function runWalletWebUiPwaLifecycleTest(t: GlobalTestState) { const pwaUrl = `http://127.0.0.1:${port}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-pwa-lifecycle", ); await waitForPwa(pwaUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-deposit.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-deposit.ts @@ -98,13 +98,19 @@ export async function runWalletWebUiPwaPeerDepositTest(t: GlobalTestState) { const receiverUrl = `http://127.0.0.1:${receiverPort}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${senderPort}`], + [ + path.join(walletWebUiDirectory, "test-server.mjs"), + `--port=${senderPort}`, + ], "wallet-webui-pwa-sender", ); await waitForPwa(senderUrl); t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${receiverPort}`], + [ + path.join(walletWebUiDirectory, "test-server.mjs"), + `--port=${receiverPort}`, + ], "wallet-webui-pwa-receiver", ); await waitForPwa(receiverUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-tos.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-tos.ts @@ -100,13 +100,16 @@ export async function runWalletWebUiPwaPeerTosTest(t: GlobalTestState) { const receiverUrl = `http://127.0.0.1:${receiverPort}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-pwa-peer-tos", ); await waitForPwa(pwaUrl); t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${receiverPort}`], + [ + path.join(walletWebUiDirectory, "test-server.mjs"), + `--port=${receiverPort}`, + ], "wallet-webui-pwa-peer-tos-receiver", ); await waitForPwa(receiverUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-sqlite.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-sqlite.ts @@ -21,6 +21,10 @@ import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; import { GlobalTestState } from "../harness/harness.js"; import { Stage } from "../stagefright/stage.js"; import { WebDriverStage } from "../stagefright/webdriver-stage.js"; +import { + findWalletWebUiDirectory, + walletWebUiArtifactPath, +} from "./wallet-web-ui-test-support.js"; async function createPwaStage( scenarioName: string, @@ -31,20 +35,6 @@ async function createPwaStage( : Stage.create(scenarioName, options); } -function findWalletWebUiDirectory(): string { - const candidates = [ - process.env.TALER_WALLET_WEBUI_DIR, - path.resolve(process.cwd(), "packages/wallet-webui"), - path.resolve(process.cwd(), "../wallet-webui"), - ].filter((candidate): candidate is string => Boolean(candidate)); - for (const candidate of candidates) { - if (fs.existsSync(path.join(candidate, "dev.mjs"))) return candidate; - } - throw Error( - "wallet-webui sources not found; run from the wallet-core checkout or set TALER_WALLET_WEBUI_DIR", - ); -} - async function reserveTcpPort(): Promise<number> { const server = net.createServer(); await new Promise<void>((resolve, reject) => { @@ -716,7 +706,7 @@ async function runPwaSqliteTest( t.spawnService( process.execPath, [ - path.join(walletWebUiDirectory, "dev.mjs"), + path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`, "--test-pwa-sqlite", ], @@ -727,10 +717,18 @@ async function runPwaSqliteTest( // the artifact measurements from this SQLite build before a later harness // host can replace dist with an ordinary PWA or extension build. const sqliteWorkerBytes = fs.statSync( - path.join(walletWebUiDirectory, "dist/pwa/sqlite-opfs-worker.js"), + walletWebUiArtifactPath( + walletWebUiDirectory, + "pwa-sqlite", + "pwa/sqlite-opfs-worker.js", + ), ).size; const sqliteWasmBytes = fs.statSync( - path.join(walletWebUiDirectory, "dist/pwa/sqlite3.wasm"), + walletWebUiArtifactPath( + walletWebUiDirectory, + "pwa-sqlite", + "pwa/sqlite3.wasm", + ), ).size; await runRealPersistencePolicyGate(t, pwaUrl, browserType); await runPrivateBrowsingGate(t, pwaUrl, browserType); 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 @@ -95,7 +95,7 @@ export async function runWalletWebUiPwaStartupTest(t: GlobalTestState) { const pwaUrl = `http://127.0.0.1:${port}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-pwa-startup", ); await waitForPwa(pwaUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-storybook.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-storybook.ts @@ -23,7 +23,7 @@ export async function runWalletWebUiPwaStorybookTest(t: GlobalTestState) { const pwaUrl = `http://127.0.0.1:${port}/`; t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-pwa-storybook", ); await waitForPwa(pwaUrl); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-withdrawal.ts @@ -43,23 +43,10 @@ import { Stage } from "../stagefright/stage.js"; import { WebDriverStage } from "../stagefright/webdriver-stage.js"; import { acknowledgeBestEffortStorageForTest, + findWalletWebUiDirectory, startCorsBridge, } from "./wallet-web-ui-test-support.js"; -function findWalletWebUiDirectory(): string { - const candidates = [ - process.env.TALER_WALLET_WEBUI_DIR, - path.resolve(process.cwd(), "packages/wallet-webui"), - path.resolve(process.cwd(), "../wallet-webui"), - ].filter((candidate): candidate is string => Boolean(candidate)); - for (const candidate of candidates) { - if (fs.existsSync(path.join(candidate, "dev.mjs"))) return candidate; - } - throw Error( - "wallet-webui sources not found; run from the wallet-core checkout or set TALER_WALLET_WEBUI_DIR", - ); -} - async function reserveTcpPort(): Promise<number> { const server = net.createServer(); await new Promise<void>((resolve, reject) => { @@ -312,7 +299,7 @@ async function runPwaWithdrawalTest( t.spawnService( process.execPath, - [path.join(walletWebUiDirectory, "dev.mjs"), `--port=${port}`], + [path.join(walletWebUiDirectory, "test-server.mjs"), `--port=${port}`], "wallet-webui-pwa", ); await waitForPwa(pwaUrl); @@ -1215,39 +1202,37 @@ async function runPwaWithdrawalTest( await page .getByRole("button", { name: "Apply experiment" }) .click(); - await page - .getByText(/was applied/) - .waitFor(); + await page.getByText(/was applied/).waitFor(); }, ); await stage.step( "clear the wallet from developer settings", async (page) => { - await page.goto(`${pwaUrl}#/settings`); - await page.getByRole("heading", { name: "Settings" }).waitFor(); - await page - .locator("summary", { hasText: "Developer settings" }) - .click(); - await page - .getByRole("button", { name: "Clear wallet database" }) - .click(); + await page.goto(`${pwaUrl}#/settings`); + await page.getByRole("heading", { name: "Settings" }).waitFor(); + await page + .locator("summary", { hasText: "Developer settings" }) + .click(); + await page + .getByRole("button", { name: "Clear wallet database" }) + .click(); const dialog = page.getByRole("dialog", { name: "Clear the wallet database?", }); await dialog - .getByRole("checkbox", { name: /permanently destroys/ }) - .check(); + .getByRole("checkbox", { name: /permanently destroys/ }) + .check(); await dialog - .getByRole("button", { name: "Clear wallet database" }) - .click(); + .getByRole("button", { name: "Clear wallet database" }) + .click(); await page - .getByText("Wallet database cleared.", { exact: true }) - .waitFor(); - await page.goto(`${pwaUrl}#/`); + .getByText("Wallet database cleared.", { exact: true }) + .waitFor(); + await page.goto(`${pwaUrl}#/`); await page - .getByRole("heading", { name: "Welcome to Taler Wallet!" }) - .waitFor(); + .getByRole("heading", { name: "Welcome to Taler Wallet!" }) + .waitFor(); }, ); } diff --git a/packages/taler-harness/src/integrationtests/wallet-web-ui-test-support.ts b/packages/taler-harness/src/integrationtests/wallet-web-ui-test-support.ts @@ -8,37 +8,71 @@ */ import fs from "node:fs"; +import { execFileSync } from "node:child_process"; import http from "node:http"; import net from "node:net"; import path from "node:path"; import { delayMs } from "../harness/harness.js"; +declare const __INSTALL_PREFIX__: string; + /** Opt a workflow test into the PWA's explicit best-effort storage mode. */ export function acknowledgeBestEffortStorageForTest(): void { ( globalThis as unknown as { sessionStorage: { setItem(key: string, value: string): void }; } - ).sessionStorage.setItem( - "gnu-taler-wallet-best-effort-acknowledged", - "yes", - ); + ).sessionStorage.setItem("gnu-taler-wallet-best-effort-acknowledged", "yes"); } export function findWalletWebUiDirectory(): string { const candidates = [ process.env.TALER_WALLET_WEBUI_DIR, + path.join(__INSTALL_PREFIX__, "share/taler-harness/webui/wallet-webui"), path.resolve(process.cwd(), "packages/wallet-webui"), path.resolve(process.cwd(), "../wallet-webui"), ].filter((candidate): candidate is string => Boolean(candidate)); for (const candidate of candidates) { - if (fs.existsSync(path.join(candidate, "dev.mjs"))) return candidate; + if (!fs.existsSync(path.join(candidate, "test-server.mjs"))) continue; + const defaultArtifact = walletWebUiArtifactPath( + candidate, + "default", + "pwa/index.html", + ); + if ( + !fs.existsSync(defaultArtifact) && + fs.existsSync(path.join(candidate, "build-harness.mjs")) + ) { + execFileSync( + process.execPath, + [path.join(candidate, "build-harness.mjs")], + { + cwd: candidate, + stdio: "inherit", + }, + ); + } + if (fs.existsSync(defaultArtifact)) return candidate; } throw Error( - "wallet-webui sources not found; run from the wallet-core checkout or set TALER_WALLET_WEBUI_DIR", + `wallet-webui test artifacts not found below ${__INSTALL_PREFIX__}/share; install them, run from the wallet-core checkout, or set TALER_WALLET_WEBUI_DIR`, ); } +export type WalletWebUiArtifactVariant = + | "default" + | "pwa-sqlite" + | "extension-sqlite" + | "legacy"; + +export function walletWebUiArtifactPath( + walletWebUiDirectory: string, + variant: WalletWebUiArtifactVariant, + ...entries: string[] +): string { + return path.join(walletWebUiDirectory, "dist/harness", variant, ...entries); +} + export async function reserveTcpPort(): Promise<number> { const server = net.createServer(); await new Promise<void>((resolve, reject) => { diff --git a/packages/wallet-webui/Makefile b/packages/wallet-webui/Makefile @@ -0,0 +1,31 @@ +# This Makefile has been placed in the public domain. + +ifeq ($(TOPLEVEL), yes) + -include ../../.config.mk + override DESTDIR := $(TOP_DESTDIR) +else + -include ../../.config.mk + -include .config.mk +endif + +fixtures_dir=$(DESTDIR)$(prefix)/share/taler-harness/webui/wallet-webui + +.PHONY: all deps install-nodeps install +all: + @echo run \'make install\' to install wallet WebUI harness artifacts + +deps: + pnpm install --frozen-lockfile --filter @gnu-taler/wallet-webui... + pnpm run --filter @gnu-taler/wallet-webui build:harness + +install-nodeps: + install -d $(fixtures_dir)/dist + install test-server.mjs $(fixtures_dir)/ + rm -rf $(fixtures_dir)/dist/harness + cp -a dist/harness $(fixtures_dir)/dist/ + +install: +ifneq ($(TALER_PACKAGING_PREBUILT),1) + $(MAKE) deps +endif + $(MAKE) install-nodeps diff --git a/packages/wallet-webui/build-harness.mjs b/packages/wallet-webui/build-harness.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { cpSync, mkdtempSync, mkdirSync, rmSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDirectory = fileURLToPath(new URL("./", import.meta.url)); +const buildScript = path.join(packageDirectory, "build.mjs"); +const staging = mkdtempSync(path.join(os.tmpdir(), "taler-wallet-webui-")); + +function build(...args) { + execFileSync(process.execPath, [buildScript, ...args], { + cwd: packageDirectory, + stdio: "inherit", + }); +} + +function stage(source, destination) { + const target = path.join(staging, destination); + mkdirSync(path.dirname(target), { recursive: true }); + cpSync(path.join(packageDirectory, source), target, { recursive: true }); +} + +try { + build(); + stage("dist/pwa", "default/pwa"); + stage("dist/demo", "default/demo"); + stage("dist/extension", "default/extension"); + + build("--test-pwa-sqlite"); + stage("dist/pwa", "pwa-sqlite/pwa"); + + build("--test-extension-sqlite"); + stage("dist/extension", "extension-sqlite/extension"); + + mkdirSync(path.join(staging, "legacy"), { recursive: true }); + execFileSync( + path.join(packageDirectory, "node_modules/.bin/esbuild"), + [ + path.join(packageDirectory, "src/testing/legacy-extension-background.ts"), + "--bundle", + "--platform=browser", + "--format=iife", + "--conditions=browser", + `--outfile=${path.join(staging, "legacy/background.js")}`, + ], + { cwd: packageDirectory, stdio: "inherit" }, + ); + + // Leave the ordinary release build in dist/ for the usual packaging tasks. + build(); + const output = path.join(packageDirectory, "dist/harness"); + rmSync(output, { recursive: true, force: true }); + cpSync(staging, output, { recursive: true }); +} finally { + rmSync(staging, { recursive: true, force: true }); +} diff --git a/packages/wallet-webui/package.json b/packages/wallet-webui/package.json @@ -13,6 +13,7 @@ "dev:test:pwa-sqlite": "node ./dev.mjs --test-pwa-sqlite", "dev:test:extension-sqlite": "node ./dev.mjs --test-extension-sqlite", "build": "node build.mjs", + "build:harness": "node build-harness.mjs", "build:without-sqlite": "node build.mjs --without-sqlite", "build:test:pwa-sqlite": "node build.mjs --test-pwa-sqlite", "build:test:extension-sqlite": "node build.mjs --test-extension-sqlite", @@ -27,7 +28,7 @@ "lint:firefox": "web-ext lint --source-dir dist/extension/firefox --boring", "lint": "../qa-tooling/bin/eslint.mjs .", "check": "pnpm typecheck && pnpm test:boundaries && pnpm test && pnpm build && pnpm package:extensions", - "pretty": "prettier --write src test build.mjs bundle-budget.mjs bundle-budget.d.mts pack.mjs pack.d.mts tailwind.config.mjs", + "pretty": "prettier --write src test build.mjs build-harness.mjs test-server.mjs bundle-budget.mjs bundle-budget.d.mts pack.mjs pack.d.mts tailwind.config.mjs", "i18n:source2po": "pogen extract && pogen merge", "i18n:po2strings": "pogen emit", "i18n:check": "pogen check" diff --git a/packages/wallet-webui/test-server.mjs b/packages/wallet-webui/test-server.mjs @@ -0,0 +1,87 @@ +#!/usr/bin/env node + +import { createReadStream } from "node:fs"; +import { stat } from "node:fs/promises"; +import { createServer } from "node:http"; +import { extname, join, normalize } from "node:path"; +import { fileURLToPath } from "node:url"; + +const fixtureRoot = fileURLToPath(new URL("./dist/harness/", import.meta.url)); +const portArgument = process.argv.find((value) => value.startsWith("--port=")); +const port = Number(portArgument?.slice("--port=".length) ?? 8080); +const pwaSqliteTest = process.argv.includes("--test-pwa-sqlite"); +const demoMode = process.argv.includes("--demo"); +const servedDirectory = join( + fixtureRoot, + demoMode ? "default/demo" : pwaSqliteTest ? "pwa-sqlite/pwa" : "default/pwa", +); + +if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw Error("--port must be an integer between 1 and 65535"); +} + +const contentTypes = new Map([ + [".css", "text/css; charset=utf-8"], + [".html", "text/html; charset=utf-8"], + [".js", "text/javascript; charset=utf-8"], + [".json", "application/json; charset=utf-8"], + [".svg", "image/svg+xml"], + [".wasm", "application/wasm"], + [".webmanifest", "application/manifest+json"], +]); + +const server = createServer(async (request, response) => { + try { + const requestUrl = new URL( + request.url ?? "/", + `http://${request.headers.host ?? "localhost"}`, + ); + const decoded = decodeURIComponent(requestUrl.pathname); + if (pwaSqliteTest && decoded === "/__wallet_webui_test_empty.html") { + response.writeHead(200, { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + "Content-Security-Policy": "default-src 'none'", + }); + response.end( + "<!doctype html><html><head><title>Wallet storage test</title></head><body></body></html>", + ); + return; + } + const relative = normalize(decoded).replace(/^[/\\]+/, ""); + if ( + relative === ".." || + relative.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) + ) { + response.writeHead(400).end("Invalid path"); + return; + } + let filename = join(servedDirectory, relative || "index.html"); + let info; + try { + info = await stat(filename); + } catch { + filename = join(servedDirectory, "index.html"); + info = await stat(filename); + } + if (info.isDirectory()) filename = join(filename, "index.html"); + response.writeHead(200, { + "Content-Type": + contentTypes.get(extname(filename)) ?? "application/octet-stream", + "Cache-Control": "no-store", + "Cross-Origin-Resource-Policy": "same-origin", + }); + createReadStream(filename).pipe(response); + } catch (error) { + response.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" }); + response.end(error instanceof Error ? error.message : "Test server failed"); + } +}); + +server.listen(port, "127.0.0.1", () => { + process.stdout.write(`GNU Taler Wallet test UI: http://127.0.0.1:${port}/\n`); +}); + +for (const signal of ["SIGINT", "SIGTERM"]) { + process.on(signal, () => server.close(() => process.exit(0))); +}