From 9e5ae8bcb3ad9a4347ca44eb478e177836af516b Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Fri, 13 Feb 2026 17:00:50 +0300 Subject: [PATCH 01/10] feat: add Playwright e2e test setup --- .gitignore | 2 ++ e2e/README.md | 36 ++++++++++++++++++++++++++++++++++++ e2e/example.spec.ts | 8 ++++++++ package.json | 9 +++++++-- playwright.config.ts | 19 +++++++++++++++++++ yarn.lock | 26 ++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 e2e/README.md create mode 100644 e2e/example.spec.ts create mode 100644 playwright.config.ts diff --git a/.gitignore b/.gitignore index de4d1f00..c30992e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ dist node_modules +test-results +playwright-report diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000..b39da79a --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,36 @@ +# E2E Tests + +E2E tests use [Playwright](https://playwright.dev/). + +## Environment Setup + +1. Install project dependencies: + ```bash + yarn install + ``` + +2. Install Playwright browsers: + ```bash + yarn test:e2e:install + ``` + +## Running Tests + +```bash +# All e2e tests +yarn test:e2e + +# Chromium only (faster) +yarn test:e2e --project=chromium + +# With UI for debugging +yarn test:e2e:ui + +# In headed mode (visible browser) +yarn test:e2e:headed +``` + +## Structure + +- `e2e/` — e2e test directory +- `playwright.config.ts` — Playwright configuration diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts new file mode 100644 index 00000000..752a4803 --- /dev/null +++ b/e2e/example.spec.ts @@ -0,0 +1,8 @@ +import { test, expect } from "@playwright/test"; + +test.describe("E2E setup", () => { + test("placeholder - e2e environment is ready", async ({ page }) => { + await page.goto("about:blank"); + expect(await page.evaluate(() => typeof window !== "undefined")).toBe(true); + }); +}); diff --git a/package.json b/package.json index c2726284..735a621f 100644 --- a/package.json +++ b/package.json @@ -51,10 +51,14 @@ "lint:js": "eslint src --ext .js,.ts --max-warnings 0", "lint:js:fix": "eslint src --ext .js,.ts --fix", "tsc:compile": "tsc --noEmit", - "format": "prettier --check '**/*.(md|json)' 'src/**/*.(js|ts)' 'examples/**/*.(js|jsx)'", - "format:fix": "prettier --loglevel silent --write '**/*.(md|json)' 'src/**/*.(js|ts)' 'examples/**/*.(js|jsx|ts)'", + "format": "prettier --check '**/*.(md|json)' 'src/**/*.(js|ts)' 'examples/**/*.(js|jsx)' 'e2e/**/*.(js|ts)' 'playwright.config.ts'", + "format:fix": "prettier --loglevel silent --write '**/*.(md|json)' 'src/**/*.(js|ts)' 'examples/**/*.(js|jsx|ts)' 'e2e/**/*.(js|ts)' 'playwright.config.ts'", "prepack": "yarn run build", "test": "jest", + "test:e2e": "playwright test", + "test:e2e:install": "playwright install", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed", "clean": "rm -rf dist", "build": "rollup -c", "dev": "microbundle watch", @@ -67,6 +71,7 @@ "devDependencies": { "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", + "@playwright/test": "^1.58.2", "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-terser": "^0.4.4", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..799a955a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,19 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + trace: "on-first-retry", + }, + projects: [ + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { name: "webkit", use: { ...devices["Desktop Safari"] } }, + ], + webServer: undefined, +}); diff --git a/yarn.lock b/yarn.lock index ae506dd8..8bac6504 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1190,6 +1190,13 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== +"@playwright/test@^1.58.2": + version "1.58.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.58.2.tgz#b0ad585d2e950d690ef52424967a42f40c6d2cbd" + integrity sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA== + dependencies: + playwright "1.58.2" + "@rollup/plugin-commonjs@^28.0.6": version "28.0.8" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.8.tgz#ba125055ebbb8375d2d2e465a3bedf434f5b2929" @@ -2887,6 +2894,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@^2.3.3, fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -4342,6 +4354,20 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +playwright-core@1.58.2: + version "1.58.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.58.2.tgz#ac5f5b4b10d29bcf934415f0b8d133b34b0dcb13" + integrity sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg== + +playwright@1.58.2: + version "1.58.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.58.2.tgz#afe547164539b0bcfcb79957394a7a3fa8683cfd" + integrity sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A== + dependencies: + playwright-core "1.58.2" + optionalDependencies: + fsevents "2.3.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" From 05b7a70527623ec088cd25a90283ffeb0b5787ae Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Mon, 16 Feb 2026 14:21:41 +0300 Subject: [PATCH 02/10] feat(e2e): add Amount module e2e tests --- e2e/README.md | 6 +- e2e/amount.spec.ts | 48 +++++++ e2e/fixtures/amount.html | 33 +++++ package.json | 3 +- playwright.config.ts | 8 +- yarn.lock | 288 ++++++++++++++++++++++++++++++++++++--- 6 files changed, 360 insertions(+), 26 deletions(-) create mode 100644 e2e/amount.spec.ts create mode 100644 e2e/fixtures/amount.html diff --git a/e2e/README.md b/e2e/README.md index b39da79a..688c53e6 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -17,11 +17,11 @@ E2E tests use [Playwright](https://playwright.dev/). ## Running Tests ```bash -# All e2e tests +# E2E tests (Chromium by default) yarn test:e2e -# Chromium only (faster) -yarn test:e2e --project=chromium +# All browsers (requires: yarn test:e2e:install first) +yarn playwright test # With UI for debugging yarn test:e2e:ui diff --git a/e2e/amount.spec.ts b/e2e/amount.spec.ts new file mode 100644 index 00000000..b34afe41 --- /dev/null +++ b/e2e/amount.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from "@playwright/test"; + +test.describe("lnclient/Amount", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/e2e/fixtures/amount.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + }); + + test("SATS returns amount with satoshi", async ({ page }) => { + const satoshi = await page.evaluate(async () => { + const { SATS } = await import("/dist/esm/lnclient.js"); + const amount = SATS(10); + return amount.satoshi; + }); + expect(satoshi).toBe(10); + }); + + test("resolveAmount resolves sync amount", async ({ page }) => { + const resolved = await page.evaluate(async () => { + const { resolveAmount } = await import("/dist/esm/lnclient.js"); + return await resolveAmount({ satoshi: 10 }); + }); + expect(resolved).toEqual({ satoshi: 10, millisat: 10_000 }); + }); + + test("resolveAmount resolves async amount", async ({ page }) => { + const resolved = await page.evaluate(async () => { + const { resolveAmount } = await import("/dist/esm/lnclient.js"); + return await resolveAmount({ + satoshi: new Promise((r) => setTimeout(() => r(10), 50)), + }); + }); + expect(resolved).toEqual({ satoshi: 10, millisat: 10_000 }); + }); + + test("Amount functions work via fixture", async ({ page }) => { + const results = await page.evaluate(() => { + return (window as unknown as { __runAmountTests__: () => Promise }) + .__runAmountTests__(); + }); + + expect(results).toMatchObject({ + satoshi: 10, + resolved: { satoshi: 10, millisat: 10_000 }, + resolvedAsync: { satoshi: 10, millisat: 10_000 }, + }); + }); +}); diff --git a/e2e/fixtures/amount.html b/e2e/fixtures/amount.html new file mode 100644 index 00000000..512aac74 --- /dev/null +++ b/e2e/fixtures/amount.html @@ -0,0 +1,33 @@ + + + + + Amount E2E Fixture + + +
Loading...
+ + + diff --git a/package.json b/package.json index 735a621f..ec8d3746 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "format:fix": "prettier --loglevel silent --write '**/*.(md|json)' 'src/**/*.(js|ts)' 'examples/**/*.(js|jsx|ts)' 'e2e/**/*.(js|ts)' 'playwright.config.ts'", "prepack": "yarn run build", "test": "jest", - "test:e2e": "playwright test", + "test:e2e": "playwright test --project=chromium", "test:e2e:install": "playwright install", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed", @@ -88,6 +88,7 @@ "jest": "^30.0.5", "lint-staged": "^16.1.4", "prettier": "^3.6.2", + "serve": "^14.2.4", "qrcode-terminal": "^0.12.0", "rollup": "^4.46.2", "rollup-plugin-dts": "^6.2.1", diff --git a/playwright.config.ts b/playwright.config.ts index 799a955a..c1b2f755 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: "html", use: { + baseURL: "http://localhost:3000", trace: "on-first-retry", }, projects: [ @@ -15,5 +16,10 @@ export default defineConfig({ { name: "firefox", use: { ...devices["Desktop Firefox"] } }, { name: "webkit", use: { ...devices["Desktop Safari"] } }, ], - webServer: undefined, + webServer: { + command: "yarn build && serve . -p 3000", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, }); diff --git a/yarn.lock b/yarn.lock index 8bac6504..76481200 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1774,6 +1774,11 @@ resolved "https://registry.npmjs.org/@webbtc/webln-types/-/webln-types-3.0.0.tgz" integrity sha512-aXfTHLKz5lysd+6xTeWl+qHNh/p3qVYbeLo+yDN5cUDmhie2ZoGvkppfWxzbGkcFBzb6dJyQ2/i2cbmDHas+zQ== +"@zeit/schemas@2.36.0": + version "2.36.0" + resolved "https://registry.yarnpkg.com/@zeit/schemas/-/schemas-2.36.0.tgz#7a1b53f4091e18d0b404873ea3e3c83589c765f2" + integrity sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg== + JSONStream@^1.3.5: version "1.3.5" resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" @@ -1810,6 +1815,16 @@ acorn@^8.4.1, acorn@^8.9.0: resolved "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +ajv@8.12.0, ajv@^8.11.0: + version "8.12.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -1820,15 +1835,12 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.11.0: - version "8.12.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== +ansi-align@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" + string-width "^4.1.0" ansi-escapes@^4.3.2: version "4.3.2" @@ -1886,6 +1898,16 @@ anymatch@^3.1.3: normalize-path "^3.0.0" picomatch "^2.0.4" +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + +arg@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + arg@^4.1.0: version "4.1.3" resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" @@ -1993,6 +2015,20 @@ body-parser@^2.2.0: raw-body "^3.0.0" type-is "^2.0.0" +boxen@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.0.0.tgz#9e5f8c26e716793fc96edcf7cf754cdf5e3fbf32" + integrity sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg== + dependencies: + ansi-align "^3.0.1" + camelcase "^7.0.0" + chalk "^5.0.1" + cli-boxes "^3.0.0" + string-width "^5.1.2" + type-fest "^2.13.0" + widest-line "^4.0.1" + wrap-ansi "^8.0.1" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -2061,6 +2097,11 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.3.0" +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" @@ -2097,6 +2138,11 @@ camelcase@^6.3.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +camelcase@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048" + integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw== + caniuse-lite@^1.0.30001400: version "1.0.30001420" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001420.tgz" @@ -2107,6 +2153,18 @@ caniuse-lite@^1.0.30001726: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz#277c07416ea4613ec564e5b0ffb47e7b60f32e2f" integrity sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg== +chalk-template@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-0.4.0.tgz#692c034d0ed62436b9062c1707fadcd0f753204b" + integrity sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg== + dependencies: + chalk "^4.1.2" + +chalk@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6" + integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w== + chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" @@ -2124,6 +2182,11 @@ chalk@^4.0.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.0.1: + version "5.6.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" + integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== + chalk@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" @@ -2144,6 +2207,11 @@ cjs-module-lexer@^2.1.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz#586e87d4341cb2661850ece5190232ccdebcff8b" integrity sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA== +cli-boxes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" + integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== + cli-cursor@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz" @@ -2159,6 +2227,15 @@ cli-truncate@^5.0.0: slice-ansi "^7.1.0" string-width "^8.0.0" +clipboardy@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-3.0.0.tgz#f3876247404d334c9ed01b6f269c11d09a5e3092" + integrity sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg== + dependencies: + arch "^2.2.0" + execa "^5.1.1" + is-wsl "^2.2.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" @@ -2230,11 +2307,36 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" +compressible@~2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + negotiator "~0.6.4" + on-headers "~1.1.0" + safe-buffer "5.2.1" + vary "~1.1.2" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== + content-disposition@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" @@ -2335,7 +2437,7 @@ dargs@^8.0.0: resolved "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz" integrity sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw== -debug@^2.2.0: +debug@2.6.9, debug@^2.2.0: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -2354,6 +2456,11 @@ dedent@^1.6.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.6.0.tgz#79d52d6389b1ffa67d2bcef59ba51847a9d503b2" integrity sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA== +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -3172,6 +3279,11 @@ ini@4.1.1: resolved "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz" integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" @@ -3189,6 +3301,11 @@ is-core-module@^2.16.0: dependencies: hasown "^2.0.2" +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" @@ -3238,6 +3355,11 @@ is-path-inside@^3.0.3: resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-port-reachable@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-port-reachable/-/is-port-reachable-4.0.0.tgz#dac044091ef15319c8ab2f34604d8794181f8c2d" + integrity sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig== + is-promise@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" @@ -3267,6 +3389,13 @@ is-typedarray@^1.0.0: resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -4012,11 +4141,23 @@ micromatch@^4.0.4, micromatch@^4.0.8: braces "^3.0.3" picomatch "^2.3.1" -mime-db@^1.54.0: +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: version "1.54.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== + +mime-types@2.1.18: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== + dependencies: + mime-db "~1.33.0" + mime-types@^3.0.0, mime-types@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" @@ -4034,6 +4175,13 @@ mimic-function@^5.0.0: resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== +minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + minimatch@9.0.3: version "9.0.3" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" @@ -4041,13 +4189,6 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - minimatch@^9.0.4: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" @@ -4055,7 +4196,7 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.5, minimist@^1.2.8: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -4095,6 +4236,11 @@ negotiator@^1.0.0: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -4167,6 +4313,11 @@ on-finished@^2.4.1: dependencies: ee-first "1.1.1" +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== + once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -4289,6 +4440,11 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +path-is-inside@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== + path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" @@ -4307,6 +4463,11 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== + path-to-regexp@^8.0.0: version "8.2.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" @@ -4438,6 +4599,11 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +range-parser@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== + range-parser@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -4453,11 +4619,36 @@ raw-body@^3.0.0: iconv-lite "0.6.3" unpipe "1.0.0" +rc@^1.0.1, rc@^1.1.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-is@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +registry-auth-token@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20" + integrity sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ== + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + integrity sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA== + dependencies: + rc "^1.0.1" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" @@ -4577,7 +4768,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.2.1, safe-buffer@^5.1.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4621,6 +4812,19 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" +serve-handler@6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" + integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== + dependencies: + bytes "3.0.0" + content-disposition "0.5.2" + mime-types "2.1.18" + minimatch "3.1.2" + path-is-inside "1.0.2" + path-to-regexp "3.3.0" + range-parser "1.2.0" + serve-static@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" @@ -4631,6 +4835,23 @@ serve-static@^2.2.0: parseurl "^1.3.3" send "^1.2.0" +serve@^14.2.5: + version "14.2.5" + resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.5.tgz#569e333b99a484b3a6d25acce4a569c8c4f96373" + integrity sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA== + dependencies: + "@zeit/schemas" "2.36.0" + ajv "8.12.0" + arg "5.0.2" + boxen "7.0.0" + chalk "5.0.1" + chalk-template "0.4.0" + clipboardy "3.0.0" + compression "1.8.1" + is-port-reachable "4.0.0" + serve-handler "6.1.6" + update-check "1.5.4" + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" @@ -4852,6 +5073,11 @@ strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" @@ -5012,6 +5238,11 @@ type-fest@^0.21.3: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^2.13.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-fest@^4.41.0: version "4.41.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" @@ -5111,6 +5342,14 @@ update-browserslist-db@^1.1.3: escalade "^3.2.0" picocolors "^1.1.1" +update-check@1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/update-check/-/update-check-1.5.4.tgz#5b508e259558f1ad7dbc8b4b0457d4c9d28c8743" + integrity sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ== + dependencies: + registry-auth-token "3.3.2" + registry-url "3.1.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" @@ -5139,7 +5378,7 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" -vary@^1.1.2: +vary@^1.1.2, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== @@ -5178,6 +5417,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +widest-line@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2" + integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig== + dependencies: + string-width "^5.0.1" + wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -5201,7 +5447,7 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^8.1.0: +wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== From 52a752041db0aba4ff5f89f13694a915a16d9711 Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Mon, 16 Feb 2026 14:30:38 +0300 Subject: [PATCH 03/10] feat(e2e): add FiatAmount module e2e tests with API mock --- e2e/fiat-amount.spec.ts | 75 +++++++++++++++++++++++++++++++++++ e2e/fixtures/fiat-amount.html | 29 ++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 e2e/fiat-amount.spec.ts create mode 100644 e2e/fixtures/fiat-amount.html diff --git a/e2e/fiat-amount.spec.ts b/e2e/fiat-amount.spec.ts new file mode 100644 index 00000000..e1081a5f --- /dev/null +++ b/e2e/fiat-amount.spec.ts @@ -0,0 +1,75 @@ +import { test, expect } from "@playwright/test"; + +const RATES_API = /getalby\.com\/api\/rates\/.*\.json/; + +const mockRatesResponse = { + code: "USD", + symbol: "$", + rate: "100000.00", + rate_float: 100_000, + rate_cents: 10_000_000, +}; + +test.describe("lnclient/FiatAmount", () => { + test.beforeEach(async ({ page }) => { + await page.route(RATES_API, (route) => { + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mockRatesResponse), + }); + }); + + await page.goto("/e2e/fixtures/fiat-amount.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + }); + + test("USD converts to satoshis via resolveAmount", async ({ page }) => { + const resolved = await page.evaluate(async () => { + const { USD, resolveAmount } = await import("/dist/esm/lnclient.js"); + const fiatAmount = USD(1); + return await resolveAmount(fiatAmount); + }); + + expect(resolved.satoshi).toBeGreaterThan(0); + expect(resolved.millisat).toBe(resolved.satoshi * 1000); + }); + + test("FiatAmount is interoperable with Amount", async ({ page }) => { + const resolved = await page.evaluate(async () => { + const { USD, resolveAmount } = await import("/dist/esm/lnclient.js"); + const fiatAmount = USD(1); + return await resolveAmount(fiatAmount); + }); + + expect(resolved).toMatchObject({ + satoshi: expect.any(Number), + millisat: expect.any(Number), + }); + expect(resolved.satoshi).toBe(1000); + }); + + test("EUR converts to satoshis", async ({ page }) => { + const resolved = await page.evaluate(async () => { + const { EUR, resolveAmount } = await import("/dist/esm/lnclient.js"); + const fiatAmount = EUR(10); + return await resolveAmount(fiatAmount); + }); + + expect(resolved.satoshi).toBe(10_000); + expect(resolved.millisat).toBe(10_000_000); + }); + + test("FiatAmount functions work via fixture", async ({ page }) => { + const results = await page.evaluate(() => { + return ( + window as unknown as { __runFiatAmountTests__: () => Promise } + ).__runFiatAmountTests__(); + }); + + expect(results).toMatchObject({ + usdResolved: { satoshi: 1000, millisat: 1_000_000 }, + eurResolved: { satoshi: 10_000, millisat: 10_000_000 }, + }); + }); +}); diff --git a/e2e/fixtures/fiat-amount.html b/e2e/fixtures/fiat-amount.html new file mode 100644 index 00000000..e54ee9ee --- /dev/null +++ b/e2e/fixtures/fiat-amount.html @@ -0,0 +1,29 @@ + + + + + FiatAmount E2E Fixture + + +
Loading...
+ + + From a5e1f59b5cff2388871b417bfc07f49b24b84a8f Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Mon, 16 Feb 2026 14:51:51 +0300 Subject: [PATCH 04/10] feat(e2e): add oauth Client e2e tests --- e2e/README.md | 4 ++ e2e/fixtures/oauth.html | 27 +++++++++++ e2e/oauth.spec.ts | 101 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 e2e/fixtures/oauth.html create mode 100644 e2e/oauth.spec.ts diff --git a/e2e/README.md b/e2e/README.md index 688c53e6..f8e8ce87 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -33,4 +33,8 @@ yarn test:e2e:headed ## Structure - `e2e/` — e2e test directory +- `e2e/fixtures/` — HTML fixtures for browser tests +- `e2e/amount.spec.ts` — lnclient/Amount tests +- `e2e/fiat-amount.spec.ts` — lnclient/FiatAmount tests (mocked rates API) +- `e2e/oauth.spec.ts` — oauth Client tests (mocked Alby API) - `playwright.config.ts` — Playwright configuration diff --git a/e2e/fixtures/oauth.html b/e2e/fixtures/oauth.html new file mode 100644 index 00000000..d92d44aa --- /dev/null +++ b/e2e/fixtures/oauth.html @@ -0,0 +1,27 @@ + + + + + OAuth Client E2E Fixture + + +
Loading...
+ + + diff --git a/e2e/oauth.spec.ts b/e2e/oauth.spec.ts new file mode 100644 index 00000000..836aeb10 --- /dev/null +++ b/e2e/oauth.spec.ts @@ -0,0 +1,101 @@ +import { test, expect } from "@playwright/test"; + +const ALBY_API = /api\.getalby\.com\/.*/; + +const mockBalanceResponse = { + balance: 21_000, + currency: "BTC", + unit: "sats", +}; + +const mockAccountInfoResponse = { + identifier: "test-user-123", + email: "test@example.com", + name: "Test User", + keysend_custom_key: "test-key", +}; + +test.describe("oauth", () => { + test.beforeEach(async ({ page }) => { + await page.route(ALBY_API, (route) => { + const url = route.request().url(); + + if (url.includes("/balance")) { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mockBalanceResponse), + }); + } + + if (url.includes("/user/me")) { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mockAccountInfoResponse), + }); + } + + route.continue(); + }); + + await page.goto("/e2e/fixtures/oauth.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + }); + + test("Client can be instantiated with bearer token string", async ({ + page, + }) => { + const clientCreated = await page.evaluate(async () => { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("test-token"); + return !!client.auth; + }); + + expect(clientCreated).toBe(true); + }); + + test("accountBalance returns mocked balance", async ({ page }) => { + const balance = await page.evaluate(async () => { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("test-token"); + return await client.accountBalance({}); + }); + + expect(balance).toEqual(mockBalanceResponse); + }); + + test("accountInformation returns mocked user info", async ({ page }) => { + const accountInfo = await page.evaluate(async () => { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("test-token"); + return await client.accountInformation({}); + }); + + expect(accountInfo).toEqual(mockAccountInfoResponse); + }); + + test("Client can be instantiated with OAuth2Bearer", async ({ page }) => { + const balance = await page.evaluate(async () => { + const { Client, OAuth2Bearer } = await import("/dist/esm/oauth.js"); + const auth = new OAuth2Bearer("custom-token"); + const client = new Client(auth); + return await client.accountBalance({}); + }); + + expect(balance).toEqual(mockBalanceResponse); + }); + + test("OAuth functions work via fixture", async ({ page }) => { + const results = await page.evaluate(() => { + return ( + window as unknown as { __runOAuthTests__: () => Promise } + ).__runOAuthTests__(); + }); + + expect(results).toMatchObject({ + balance: mockBalanceResponse, + accountInfo: mockAccountInfoResponse, + }); + }); +}); From 9eca030ab564c6a2d7c0b0dbb86e79ce41d440bd Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Mon, 16 Feb 2026 15:09:39 +0300 Subject: [PATCH 05/10] feat(e2e): add webln module e2e tests with mocked Alby API --- e2e/README.md | 1 + e2e/fixtures/webln.html | 37 +++++++++ e2e/webln.spec.ts | 172 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 e2e/fixtures/webln.html create mode 100644 e2e/webln.spec.ts diff --git a/e2e/README.md b/e2e/README.md index f8e8ce87..ac6d6313 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -37,4 +37,5 @@ yarn test:e2e:headed - `e2e/amount.spec.ts` — lnclient/Amount tests - `e2e/fiat-amount.spec.ts` — lnclient/FiatAmount tests (mocked rates API) - `e2e/oauth.spec.ts` — oauth Client tests (mocked Alby API) +- `e2e/webln.spec.ts` — webln OauthWeblnProvider tests (mocked Alby API) - `playwright.config.ts` — Playwright configuration diff --git a/e2e/fixtures/webln.html b/e2e/fixtures/webln.html new file mode 100644 index 00000000..ef585658 --- /dev/null +++ b/e2e/fixtures/webln.html @@ -0,0 +1,37 @@ + + + + + WebLN E2E Fixture + + +
Loading...
+ + + diff --git a/e2e/webln.spec.ts b/e2e/webln.spec.ts new file mode 100644 index 00000000..f0b2c35f --- /dev/null +++ b/e2e/webln.spec.ts @@ -0,0 +1,172 @@ +import { test, expect } from "@playwright/test"; + +const ALBY_API = /api\.getalby\.com\/.*/; + +const mockSendPaymentResponse = { + payment_hash: "test-hash", + payment_preimage: "test-preimage", +}; + +const mockKeysendResponse = { + payment_hash: "test-keysend-hash", + payment_preimage: "test-keysend-preimage", +}; + +const mockCreateInvoiceResponse = { + payment_request: "lnbc100n1test-invoice", + payment_hash: "invoice-hash", +}; + +test.describe("webln", () => { + test.beforeEach(async ({ page }) => { + await page.route(ALBY_API, (route) => { + const url = route.request().url(); + const method = route.request().method(); + + if (url.includes("/payments/bolt11") && method === "POST") { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mockSendPaymentResponse), + }); + } + + if (url.includes("/payments/keysend") && method === "POST") { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mockKeysendResponse), + }); + } + + if (url.includes("/invoices") && method === "POST") { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mockCreateInvoiceResponse), + }); + } + + route.continue(); + }); + + await page.goto("/e2e/fixtures/webln.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + }); + + test("OauthWeblnProvider can be instantiated with mock auth", async ({ + page, + }) => { + const created = await page.evaluate(async () => { + const { OauthWeblnProvider } = await import("/dist/esm/webln.js"); + const mockAuth = { + token: { access_token: "x" }, + getAuthHeader: () => ({ Authorization: "Bearer x" }), + }; + const provider = new OauthWeblnProvider({ auth: mockAuth }); + return !!provider && provider.oauth === true; + }); + + expect(created).toBe(true); + }); + + test("OauthWeblnProvider enable returns when token exists", async ({ + page, + }) => { + const result = await page.evaluate(async () => { + const { OauthWeblnProvider } = await import("/dist/esm/webln.js"); + const mockAuth = { + token: { access_token: "x" }, + getAuthHeader: () => ({ Authorization: "Bearer x" }), + }; + const provider = new OauthWeblnProvider({ auth: mockAuth }); + return await provider.enable(); + }); + + expect(result).toEqual({ enabled: true }); + }); + + test("OauthWeblnProvider getInfo returns Alby alias", async ({ page }) => { + const result = await page.evaluate(async () => { + const { OauthWeblnProvider } = await import("/dist/esm/webln.js"); + const mockAuth = { + token: { access_token: "x" }, + getAuthHeader: () => ({ Authorization: "Bearer x" }), + }; + const provider = new OauthWeblnProvider({ auth: mockAuth }); + await provider.enable(); + return await provider.getInfo(); + }); + + expect(result).toEqual({ alias: "Alby" }); + }); + + test("OauthWeblnProvider sendPayment returns preimage", async ({ page }) => { + const result = await page.evaluate(async () => { + const { OauthWeblnProvider } = await import("/dist/esm/webln.js"); + const mockAuth = { + token: { access_token: "x" }, + getAuthHeader: () => ({ Authorization: "Bearer x" }), + }; + const provider = new OauthWeblnProvider({ auth: mockAuth }); + await provider.enable(); + return await provider.sendPayment("lnbc100n1test-invoice"); + }); + + expect(result).toEqual({ preimage: "test-preimage" }); + }); + + test("OauthWeblnProvider keysend returns preimage", async ({ page }) => { + const result = await page.evaluate(async () => { + const { OauthWeblnProvider } = await import("/dist/esm/webln.js"); + const mockAuth = { + token: { access_token: "x" }, + getAuthHeader: () => ({ Authorization: "Bearer x" }), + }; + const provider = new OauthWeblnProvider({ auth: mockAuth }); + await provider.enable(); + return await provider.keysend({ + destination: "test-pubkey", + amount: 100, + }); + }); + + expect(result).toEqual({ preimage: "test-keysend-preimage" }); + }); + + test("OauthWeblnProvider makeInvoice returns payment request", async ({ + page, + }) => { + const result = await page.evaluate(async () => { + const { OauthWeblnProvider } = await import("/dist/esm/webln.js"); + const mockAuth = { + token: { access_token: "x" }, + getAuthHeader: () => ({ Authorization: "Bearer x" }), + }; + const provider = new OauthWeblnProvider({ auth: mockAuth }); + await provider.enable(); + return await provider.makeInvoice({ + amount: 100, + defaultMemo: "Test", + }); + }); + + expect(result).toEqual({ + paymentRequest: "lnbc100n1test-invoice", + }); + }); + + test("WebLN functions work via fixture", async ({ page }) => { + const results = await page.evaluate(() => { + return ( + window as unknown as { __runWeblnTests__: () => Promise } + ).__runWeblnTests__(); + }); + + expect(results).toMatchObject({ + enableResult: { enabled: true }, + getInfoResult: { alias: "Alby" }, + makeInvoiceResult: { paymentRequest: "lnbc100n1test-invoice" }, + }); + }); +}); From dcd94aa1d51634140796972b830793b8103233f1 Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Mon, 16 Feb 2026 15:22:03 +0300 Subject: [PATCH 06/10] feat(e2e): add nwc NWCClient and NWAClient e2e tests --- e2e/README.md | 1 + e2e/fixtures/nwc.html | 47 +++++++++++ e2e/nwc.spec.ts | 192 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 e2e/fixtures/nwc.html create mode 100644 e2e/nwc.spec.ts diff --git a/e2e/README.md b/e2e/README.md index ac6d6313..b28a2b7e 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -38,4 +38,5 @@ yarn test:e2e:headed - `e2e/fiat-amount.spec.ts` — lnclient/FiatAmount tests (mocked rates API) - `e2e/oauth.spec.ts` — oauth Client tests (mocked Alby API) - `e2e/webln.spec.ts` — webln OauthWeblnProvider tests (mocked Alby API) +- `e2e/nwc.spec.ts` — nwc NWCClient/NWAClient tests (no relay required) - `playwright.config.ts` — Playwright configuration diff --git a/e2e/fixtures/nwc.html b/e2e/fixtures/nwc.html new file mode 100644 index 00000000..de1c7543 --- /dev/null +++ b/e2e/fixtures/nwc.html @@ -0,0 +1,47 @@ + + + + + NWC E2E Fixture + + +
Loading...
+ + + diff --git a/e2e/nwc.spec.ts b/e2e/nwc.spec.ts new file mode 100644 index 00000000..a11c052a --- /dev/null +++ b/e2e/nwc.spec.ts @@ -0,0 +1,192 @@ +import { test, expect } from "@playwright/test"; + +const exampleNwcUrl = + "nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9?relay=wss://relay.getalby.com/v1&relay=wss://relay2.getalby.com/v1&secret=e839faf78693765b3833027fefa5a305c78f6965d0a5d2e47a3fcb25aa7cc45b&lud16=hello@getalby.com"; + +test.describe("nwc", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/e2e/fixtures/nwc.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + }); + + test.describe("NWCClient.parseWalletConnectUrl", () => { + test("parses standard protocol URL", async ({ page }) => { + const parsed = await page.evaluate(async (url) => { + const { NWCClient } = await import("/dist/esm/nwc.js"); + return NWCClient.parseWalletConnectUrl(url); + }, exampleNwcUrl); + + expect(parsed).toMatchObject({ + walletPubkey: + "69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", + secret: + "e839faf78693765b3833027fefa5a305c78f6965d0a5d2e47a3fcb25aa7cc45b", + relayUrls: [ + "wss://relay.getalby.com/v1", + "wss://relay2.getalby.com/v1", + ], + lud16: "hello@getalby.com", + }); + }); + + test("parses protocol without double slash", async ({ page }) => { + const urlWithoutSlash = exampleNwcUrl.replace( + "nostr+walletconnect://", + "nostr+walletconnect:", + ); + const parsed = await page.evaluate(async (url) => { + const { NWCClient } = await import("/dist/esm/nwc.js"); + return NWCClient.parseWalletConnectUrl(url); + }, urlWithoutSlash); + + expect(parsed.walletPubkey).toBe( + "69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", + ); + }); + }); + + test.describe("NWCClient constructor", () => { + test("creates client from nostrWalletConnectUrl", async ({ page }) => { + const result = await page.evaluate(async (url) => { + const { NWCClient } = await import("/dist/esm/nwc.js"); + const client = new NWCClient({ nostrWalletConnectUrl: url }); + return { + walletPubkey: client.walletPubkey, + lud16: client.lud16, + }; + }, exampleNwcUrl); + + expect(result).toEqual({ + walletPubkey: + "69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", + lud16: "hello@getalby.com", + }); + }); + + test("getPublicKey returns hex pubkey", async ({ page }) => { + const publicKey = await page.evaluate(async (url) => { + const { NWCClient } = await import("/dist/esm/nwc.js"); + const client = new NWCClient({ nostrWalletConnectUrl: url }); + return await client.getPublicKey(); + }, exampleNwcUrl); + + expect(publicKey).toMatch(/^[a-f0-9]{64}$/); + }); + + test("getNostrWalletConnectUrl returns valid URL", async ({ page }) => { + const nwcUrl = await page.evaluate(async (url) => { + const { NWCClient } = await import("/dist/esm/nwc.js"); + const client = new NWCClient({ nostrWalletConnectUrl: url }); + return client.getNostrWalletConnectUrl(); + }, exampleNwcUrl); + + expect(nwcUrl).toMatch(/^nostr\+walletconnect:\/\//); + expect(nwcUrl).toContain("relay="); + expect(nwcUrl).toContain("secret="); + }); + }); + + test.describe("NWCClient.getAuthorizationUrl", () => { + test("generates authorization URL with options", async ({ page }) => { + const authUrl = await page.evaluate(async () => { + const { NWCClient } = await import("/dist/esm/nwc.js"); + const pubkey = + "c5dc47856f533dad6c016b979ee3b21f83f88ae0f0058001b67a4b348339fe94"; + const url = NWCClient.getAuthorizationUrl( + "https://nwc.getalby.com/apps/new", + { + name: "TestApp", + returnTo: "https://example.com", + }, + pubkey, + ); + return url.toString(); + }); + + expect(authUrl).toContain("https://nwc.getalby.com/apps/new"); + expect(authUrl).toContain("name=TestApp"); + expect(authUrl).toContain("return_to="); + expect(authUrl).toContain("pubkey=c5dc47856f533dad6c016b979ee3b21f83f88ae0f0058001b67a4b348339fe94"); + }); + + test("throws for hash router paths", async ({ page }) => { + const error = await page.evaluate(async () => { + try { + const { NWCClient } = await import("/dist/esm/nwc.js"); + const pubkey = + "c5dc47856f533dad6c016b979ee3b21f83f88ae0f0058001b67a4b348339fe94"; + NWCClient.getAuthorizationUrl( + "https://my.albyhub.com/#/apps/new", + {}, + pubkey, + ); + return null; + } catch (e) { + return (e as Error).message; + } + }); + + expect(error).toBe("hash router paths not supported"); + }); + }); + + test.describe("NWAClient", () => { + test("creates client with options", async ({ page }) => { + const result = await page.evaluate(async () => { + const { NWAClient } = await import("/dist/esm/nwc.js"); + const client = new NWAClient({ + relayUrls: ["wss://relay.getalby.com/v1"], + requestMethods: ["pay_invoice", "get_balance"], + name: "TestNWA", + }); + return { + hasAppPubkey: !!client.options.appPubkey, + requestMethods: client.options.requestMethods, + name: client.options.name, + }; + }); + + expect(result).toEqual({ + hasAppPubkey: true, + requestMethods: ["pay_invoice", "get_balance"], + name: "TestNWA", + }); + }); + + test("getConnectionUri returns valid NWA URI", async ({ page }) => { + const uri = await page.evaluate(async () => { + const { NWAClient } = await import("/dist/esm/nwc.js"); + const client = new NWAClient({ + relayUrls: ["wss://relay.getalby.com/v1"], + requestMethods: ["pay_invoice"], + name: "TestNWA", + }); + return client.getConnectionUri(); + }); + + expect(uri).toMatch(/^nostr\+walletauth/); + expect(uri).toContain("relay="); + expect(uri).toContain("request_methods="); + }); + }); + + test("NWC functions work via fixture", async ({ page }) => { + const results = await page.evaluate(() => { + return ( + window as unknown as { __runNwcTests__: () => Promise } + ).__runNwcTests__(); + }); + + expect(results).toMatchObject({ + parsed: { + walletPubkey: + "69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", + lud16: "hello@getalby.com", + }, + walletPubkey: + "69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", + authUrl: expect.stringContaining("nwc.getalby.com"), + connectionUri: expect.stringContaining("nostr+walletauth"), + }); + }); +}); From d66f28b46e723138a0c6cce3af03bdfa81455868 Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Mon, 16 Feb 2026 15:53:12 +0300 Subject: [PATCH 07/10] feat(e2e): add lnclient LNClient e2e tests, remove placeholder and add oauth error handling tests --- e2e/README.md | 10 ++++ e2e/example.spec.ts | 8 --- e2e/fixtures/lnclient.html | 48 +++++++++++++++++ e2e/lnclient.spec.ts | 107 +++++++++++++++++++++++++++++++++++++ e2e/oauth.spec.ts | 102 +++++++++++++++++++++++++++++++++++ 5 files changed, 267 insertions(+), 8 deletions(-) delete mode 100644 e2e/example.spec.ts create mode 100644 e2e/fixtures/lnclient.html create mode 100644 e2e/lnclient.spec.ts diff --git a/e2e/README.md b/e2e/README.md index b28a2b7e..e082cd6c 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -39,4 +39,14 @@ yarn test:e2e:headed - `e2e/oauth.spec.ts` — oauth Client tests (mocked Alby API) - `e2e/webln.spec.ts` — webln OauthWeblnProvider tests (mocked Alby API) - `e2e/nwc.spec.ts` — nwc NWCClient/NWAClient tests (no relay required) +- `e2e/lnclient.spec.ts` — lnclient LNClient tests (constructor, close) - `playwright.config.ts` — Playwright configuration + +## Limitations + +The following are **not covered** by E2E tests (require real infrastructure or complex mocks): + +- **NWC relay-dependent flows** — `NWCClient.getInfo`, `getBalance`, `payInvoice`, `makeInvoice`, etc. require WebSocket connection to Nostr relay +- **LNClient.pay / requestPayment** — depend on NWC relay +- **NostrWeblnProvider** — depends on NWC relay +- **OAuth2User** — OAuth authorization flow with redirects and popups diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts deleted file mode 100644 index 752a4803..00000000 --- a/e2e/example.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("E2E setup", () => { - test("placeholder - e2e environment is ready", async ({ page }) => { - await page.goto("about:blank"); - expect(await page.evaluate(() => typeof window !== "undefined")).toBe(true); - }); -}); diff --git a/e2e/fixtures/lnclient.html b/e2e/fixtures/lnclient.html new file mode 100644 index 00000000..94d531b9 --- /dev/null +++ b/e2e/fixtures/lnclient.html @@ -0,0 +1,48 @@ + + + + + LNClient E2E Fixture + + +
Loading...
+ + + diff --git a/e2e/lnclient.spec.ts b/e2e/lnclient.spec.ts new file mode 100644 index 00000000..3e5110da --- /dev/null +++ b/e2e/lnclient.spec.ts @@ -0,0 +1,107 @@ +import { test, expect } from "@playwright/test"; + +const exampleNwcUrl = + "nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9?relay=wss://relay.getalby.com/v1&relay=wss://relay2.getalby.com/v1&secret=e839faf78693765b3833027fefa5a305c78f6965d0a5d2e47a3fcb25aa7cc45b&lud16=hello@getalby.com"; + +test.describe("lnclient/LNClient", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/e2e/fixtures/lnclient.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + }); + + test.describe("constructor", () => { + test("creates LNClient from NWC URL string", async ({ page }) => { + const result = await page.evaluate(async (url) => { + const { LNClient } = await import("/dist/esm/lnclient.js"); + const client = new LNClient(url); + const walletPubkey = client.nwcClient.walletPubkey; + client.close(); + return { walletPubkey }; + }, exampleNwcUrl); + + expect(result.walletPubkey).toBe( + "69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", + ); + }); + + test("creates LNClient from NWCClient instance", async ({ page }) => { + const result = await page.evaluate(async (url) => { + const { LNClient } = await import("/dist/esm/lnclient.js"); + const { NWCClient } = await import("/dist/esm/nwc.js"); + const nwcClient = new NWCClient({ nostrWalletConnectUrl: url }); + const client = new LNClient(nwcClient); + const walletPubkey = client.nwcClient.walletPubkey; + client.close(); + return { walletPubkey }; + }, exampleNwcUrl); + + expect(result.walletPubkey).toBe( + "69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", + ); + }); + + test("creates LNClient from options object", async ({ page }) => { + const result = await page.evaluate(async (url) => { + const { LNClient } = await import("/dist/esm/lnclient.js"); + const client = new LNClient({ nostrWalletConnectUrl: url }); + const hasNwc = !!client.nwcClient; + client.close(); + return { hasNwc }; + }, exampleNwcUrl); + + expect(result.hasNwc).toBe(true); + }); + + test("LN alias works same as LNClient", async ({ page }) => { + const result = await page.evaluate(async (url) => { + const { LN } = await import("/dist/esm/lnclient.js"); + const client = new LN(url); + const walletPubkey = client.nwcClient.walletPubkey; + client.close(); + return { walletPubkey }; + }, exampleNwcUrl); + + expect(result.walletPubkey).toBe( + "69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", + ); + }); + }); + + test("close does not throw", async ({ page }) => { + const closed = await page.evaluate(async (url) => { + const { LNClient } = await import("/dist/esm/lnclient.js"); + const client = new LNClient(url); + try { + client.close(); + return true; + } catch { + return false; + } + }, exampleNwcUrl); + + expect(closed).toBe(true); + }); + + test("LNClient functions work via fixture", async ({ page }) => { + const results = await page.evaluate(() => { + return ( + window as unknown as { __runLnClientTests__: () => Promise } + ).__runLnClientTests__(); + }); + + expect(results).toMatchObject({ + fromString: { + hasNwcClient: true, + walletPubkey: + "69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", + }, + fromNwc: { + hasNwcClient: true, + sameWalletPubkey: true, + }, + fromOptions: { + hasNwcClient: true, + }, + }); + }); +}); diff --git a/e2e/oauth.spec.ts b/e2e/oauth.spec.ts index 836aeb10..78dd79a9 100644 --- a/e2e/oauth.spec.ts +++ b/e2e/oauth.spec.ts @@ -99,3 +99,105 @@ test.describe("oauth", () => { }); }); }); + +test.describe("oauth error handling", () => { + test("AlbyResponseError is thrown on 4xx API response", async ({ + page, + }) => { + await page.route(/api\.getalby\.com\/.*/, (route) => { + if (route.request().url().includes("/balance")) { + return route.fulfill({ + status: 401, + contentType: "application/json", + body: JSON.stringify({ message: "Invalid token" }), + }); + } + route.continue(); + }); + + await page.goto("/e2e/fixtures/oauth.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + + const errorInfo = await page.evaluate(async () => { + try { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("invalid-token"); + await client.accountBalance({}); + return null; + } catch (e) { + const err = e as { status?: number; message?: string; error?: unknown }; + return { + status: err.status, + message: err.message, + error: err.error, + }; + } + }); + + expect(errorInfo).not.toBeNull(); + expect(errorInfo?.status).toBe(401); + expect(errorInfo?.message).toContain("401"); + expect(errorInfo?.message).toContain("Invalid token"); + expect(errorInfo?.error).toMatchObject({ message: "Invalid token" }); + }); + + test("AlbyResponseError is thrown on 5xx API response", async ({ + page, + }) => { + await page.route(/api\.getalby\.com\/.*/, (route) => { + if (route.request().url().includes("/balance")) { + return route.fulfill({ + status: 500, + contentType: "application/json", + body: JSON.stringify({ message: "Something went wrong" }), + }); + } + route.continue(); + }); + + await page.goto("/e2e/fixtures/oauth.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + + const errorInfo = await page.evaluate(async () => { + try { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("test-token"); + await client.accountBalance({}); + return null; + } catch (e) { + const err = e as { status?: number; message?: string }; + return { status: err.status, message: err.message }; + } + }); + + expect(errorInfo).not.toBeNull(); + expect(errorInfo?.status).toBe(500); + expect(errorInfo?.message).toContain("500"); + expect(errorInfo?.message).toContain("Something went wrong"); + }); + + test("network error is propagated", async ({ page }) => { + await page.route(/api\.getalby\.com\/.*/, (route) => { + if (route.request().url().includes("/balance")) { + return route.abort("failed"); + } + route.continue(); + }); + + await page.goto("/e2e/fixtures/oauth.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + + const threw = await page.evaluate(async () => { + try { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("test-token"); + await client.accountBalance({}); + return false; + } catch { + return true; + } + }); + + expect(threw).toBe(true); + }); +}); From 27ca6b6f430084348bbf864a4d036bce1136fd1e Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Mon, 16 Feb 2026 16:29:04 +0300 Subject: [PATCH 08/10] feat(e2e): add oauth signMessage, createInvoice, decodeInvoice and base_url tests --- e2e/oauth.spec.ts | 99 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/e2e/oauth.spec.ts b/e2e/oauth.spec.ts index 78dd79a9..8100da2f 100644 --- a/e2e/oauth.spec.ts +++ b/e2e/oauth.spec.ts @@ -15,6 +15,22 @@ const mockAccountInfoResponse = { keysend_custom_key: "test-key", }; +const mockSignMessageResponse = { + message: "test-message", + signature: "mock-signature-123", +}; + +const mockCreateInvoiceResponse = { + payment_request: "lnbc100n1test-invoice", + payment_hash: "invoice-hash-123", +}; + +const mockDecodedInvoiceResponse = { + currency: "BTC", + amount: 100, + payment_hash: "hash123", +}; + test.describe("oauth", () => { test.beforeEach(async ({ page }) => { await page.route(ALBY_API, (route) => { @@ -36,6 +52,30 @@ test.describe("oauth", () => { }); } + if (url.includes("/signatures") && route.request().method() === "POST") { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mockSignMessageResponse), + }); + } + + if (url.includes("/invoices") && route.request().method() === "POST") { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mockCreateInvoiceResponse), + }); + } + + if (url.includes("/decode/bolt11/")) { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(mockDecodedInvoiceResponse), + }); + } + route.continue(); }); @@ -98,6 +138,65 @@ test.describe("oauth", () => { accountInfo: mockAccountInfoResponse, }); }); + + test("signMessage returns mocked signature", async ({ page }) => { + const result = await page.evaluate(async () => { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("test-token"); + return await client.signMessage({ message: "Hello" }); + }); + + expect(result).toEqual(mockSignMessageResponse); + }); + + test("createInvoice returns mocked invoice", async ({ page }) => { + const result = await page.evaluate(async () => { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("test-token"); + return await client.createInvoice({ + amount: 100, + description: "Test invoice", + }); + }); + + expect(result).toEqual(mockCreateInvoiceResponse); + }); + + test("decodeInvoice returns mocked decoded invoice", async ({ page }) => { + const result = await page.evaluate(async () => { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("test-token"); + return await client.decodeInvoice("lnbc100n1test"); + }); + + expect(result).toEqual(mockDecodedInvoiceResponse); + }); + + test("Client uses custom base_url", async ({ page }) => { + await page.route(/custom-api\.test\/.*/, (route) => { + if (route.request().url().includes("/balance")) { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ ...mockBalanceResponse, balance: 999 }), + }); + } + route.continue(); + }); + + await page.goto("/e2e/fixtures/oauth.html"); + await page.waitForSelector("#app:has-text('Ready')", { timeout: 10000 }); + + const balance = await page.evaluate(async () => { + const { Client } = await import("/dist/esm/oauth.js"); + const client = new Client("test-token", { + base_url: "https://custom-api.test", + }); + return await client.accountBalance({}); + }); + + expect(balance).toMatchObject({ balance: 999 }); + }); }); test.describe("oauth error handling", () => { From 38ea45d174d6cbce2a6862e14a5cdde7a6ea4577 Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Mon, 16 Feb 2026 16:52:10 +0300 Subject: [PATCH 09/10] test(e2e): add resolveAmount number argument test --- e2e/amount.spec.ts | 10 ++++++++++ e2e/fixtures/amount.html | 3 +++ 2 files changed, 13 insertions(+) diff --git a/e2e/amount.spec.ts b/e2e/amount.spec.ts index b34afe41..13d434e9 100644 --- a/e2e/amount.spec.ts +++ b/e2e/amount.spec.ts @@ -33,6 +33,15 @@ test.describe("lnclient/Amount", () => { expect(resolved).toEqual({ satoshi: 10, millisat: 10_000 }); }); + test("resolveAmount accepts number directly", async ({ page }) => { + const resolved = await page.evaluate(async () => { + const { resolveAmount } = await import("/dist/esm/lnclient.js"); + return await resolveAmount(21); + }); + + expect(resolved).toEqual({ satoshi: 21, millisat: 21_000 }); + }); + test("Amount functions work via fixture", async ({ page }) => { const results = await page.evaluate(() => { return (window as unknown as { __runAmountTests__: () => Promise }) @@ -43,6 +52,7 @@ test.describe("lnclient/Amount", () => { satoshi: 10, resolved: { satoshi: 10, millisat: 10_000 }, resolvedAsync: { satoshi: 10, millisat: 10_000 }, + resolvedNumber: { satoshi: 21, millisat: 21_000 }, }); }); }); diff --git a/e2e/fixtures/amount.html b/e2e/fixtures/amount.html index 512aac74..6ccf88a3 100644 --- a/e2e/fixtures/amount.html +++ b/e2e/fixtures/amount.html @@ -24,6 +24,9 @@ satoshi: new Promise((resolve) => setTimeout(() => resolve(10), 50)), }); + // resolveAmount with number + results.resolvedNumber = await resolveAmount(21); + return results; }; From 03bef7bae7f120379782ea563cab2a3b08c9ec84 Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Tue, 17 Feb 2026 09:05:58 +0300 Subject: [PATCH 10/10] ci: add e2e tests to CI workflow --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 582b2eaa..da18292e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,3 +19,7 @@ jobs: - run: yarn install --frozen-lockfile - run: yarn build - run: yarn test + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + - name: Run E2E tests + run: yarn test:e2e