Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
dist
node_modules
test-results
playwright-report
52 changes: 52 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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
# E2E tests (Chromium by default)
yarn test:e2e

# All browsers (requires: yarn test:e2e:install first)
yarn playwright test

# With UI for debugging
yarn test:e2e:ui

# In headed mode (visible browser)
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)
- `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
58 changes: 58 additions & 0 deletions e2e/amount.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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("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<unknown> })
.__runAmountTests__();
});

expect(results).toMatchObject({
satoshi: 10,
resolved: { satoshi: 10, millisat: 10_000 },
resolvedAsync: { satoshi: 10, millisat: 10_000 },
resolvedNumber: { satoshi: 21, millisat: 21_000 },
});
});
});
75 changes: 75 additions & 0 deletions e2e/fiat-amount.spec.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> }
).__runFiatAmountTests__();
});

expect(results).toMatchObject({
usdResolved: { satoshi: 1000, millisat: 1_000_000 },
eurResolved: { satoshi: 10_000, millisat: 10_000_000 },
});
});
});
36 changes: 36 additions & 0 deletions e2e/fixtures/amount.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Amount E2E Fixture</title>
</head>
<body>
<div id="app">Loading...</div>
<script type="module">
import { SATS, resolveAmount } from "/dist/esm/lnclient.js";

window.__runAmountTests__ = async function () {
const results = {};

// SATS
const amount = SATS(10);
results.satoshi = amount.satoshi;

// resolveAmount sync
results.resolved = await resolveAmount({ satoshi: 10 });

// resolveAmount async
results.resolvedAsync = await resolveAmount({
satoshi: new Promise((resolve) => setTimeout(() => resolve(10), 50)),
});

// resolveAmount with number
results.resolvedNumber = await resolveAmount(21);

return results;
};

document.getElementById("app").textContent = "Ready";
</script>
</body>
</html>
29 changes: 29 additions & 0 deletions e2e/fixtures/fiat-amount.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>FiatAmount E2E Fixture</title>
</head>
<body>
<div id="app">Loading...</div>
<script type="module">
import { USD, EUR, resolveAmount } from "/dist/esm/lnclient.js";

window.__runFiatAmountTests__ = async function () {
const results = {};

// USD(1) - 1 USD
const usdAmount = USD(1);
results.usdResolved = await resolveAmount(usdAmount);

// EUR(10) - 10 EUR
const eurAmount = EUR(10);
results.eurResolved = await resolveAmount(eurAmount);

return results;
};

document.getElementById("app").textContent = "Ready";
</script>
</body>
</html>
48 changes: 48 additions & 0 deletions e2e/fixtures/lnclient.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>LNClient E2E Fixture</title>
</head>
<body>
<div id="app">Loading...</div>
<script type="module">
import { LNClient, LN } from "/dist/esm/lnclient.js";
import { NWCClient } from "/dist/esm/nwc.js";

const exampleNwcUrl =
"nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9?relay=wss://relay.getalby.com/v1&relay=wss://relay2.getalby.com/v1&secret=e839faf78693765b3833027fefa5a305c78f6965d0a5d2e47a3fcb25aa7cc45b&lud16=hello@getalby.com";

window.__runLnClientTests__ = async function () {
const fromString = new LNClient(exampleNwcUrl);
const fromNwc = new LNClient(new NWCClient({ nostrWalletConnectUrl: exampleNwcUrl }));
const fromOptions = new LNClient({
nostrWalletConnectUrl: exampleNwcUrl,
});

const results = {
fromString: {
hasNwcClient: !!fromString.nwcClient,
walletPubkey: fromString.nwcClient.walletPubkey,
},
fromNwc: {
hasNwcClient: !!fromNwc.nwcClient,
sameWalletPubkey:
fromNwc.nwcClient.walletPubkey === fromString.nwcClient.walletPubkey,
},
fromOptions: {
hasNwcClient: !!fromOptions.nwcClient,
},
};

fromString.close();
fromNwc.close();
fromOptions.close();

return results;
};

document.getElementById("app").textContent = "Ready";
</script>
</body>
</html>
47 changes: 47 additions & 0 deletions e2e/fixtures/nwc.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>NWC E2E Fixture</title>
</head>
<body>
<div id="app">Loading...</div>
<script type="module">
import { NWCClient, NWAClient } from "/dist/esm/nwc.js";

const exampleNwcUrl =
"nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9?relay=wss://relay.getalby.com/v1&relay=wss://relay2.getalby.com/v1&secret=e839faf78693765b3833027fefa5a305c78f6965d0a5d2e47a3fcb25aa7cc45b&lud16=hello@getalby.com";

window.__runNwcTests__ = async function () {
const parsed = NWCClient.parseWalletConnectUrl(exampleNwcUrl);
const client = new NWCClient({ nostrWalletConnectUrl: exampleNwcUrl });
const publicKey = await client.getPublicKey();
const nwcUrl = client.getNostrWalletConnectUrl();

const authUrl = NWCClient.getAuthorizationUrl(
"https://nwc.getalby.com/apps/new",
{ name: "TestApp", returnTo: "https://example.com" },
publicKey,
);

const nwaClient = new NWAClient({
relayUrls: ["wss://relay.getalby.com/v1"],
requestMethods: ["pay_invoice", "get_balance"],
name: "TestNWA",
});
const connectionUri = nwaClient.getConnectionUri();

return {
parsed,
walletPubkey: client.walletPubkey,
publicKey,
nwcUrl: nwcUrl.substring(0, 50) + "...",
authUrl: authUrl.toString(),
connectionUri: connectionUri.substring(0, 50) + "...",
};
};

document.getElementById("app").textContent = "Ready";
</script>
</body>
</html>
27 changes: 27 additions & 0 deletions e2e/fixtures/oauth.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OAuth Client E2E Fixture</title>
</head>
<body>
<div id="app">Loading...</div>
<script type="module">
import { Client, OAuth2Bearer } from "/dist/esm/oauth.js";

window.__runOAuthTests__ = async function () {
const client = new Client("test-bearer-token");

const balance = await client.accountBalance({});
const accountInfo = await client.accountInformation({});

return {
balance,
accountInfo,
};
};

document.getElementById("app").textContent = "Ready";
</script>
</body>
</html>
Loading