Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3fe7abf
feat(payment-link): show the payment QR on desktop, wallet copy on mo…
joshuakrueger-dfx Aug 6, 2026
e79d73a
test(payment-link): anchor the quote expiry fixture to now
joshuakrueger-dfx Aug 6, 2026
f4c7bec
fix(payment-link): decide the QR by pointer type, not by user agent a…
joshuakrueger-dfx Aug 7, 2026
8635b8d
feat(payment-routes): rename the QR setting to what it now does
joshuakrueger-dfx Aug 7, 2026
88ce3d8
fix(i18n): follow the file's own address form and stop leaving wallet…
joshuakrueger-dfx Aug 7, 2026
8d89901
test(handbook): cover both device variants of the payment page
joshuakrueger-dfx Aug 7, 2026
5a1c927
fix(payment-link): keep a QR reachable when the standard is not OpenC…
joshuakrueger-dfx Aug 7, 2026
f27c4b0
test(handbook): make the payment baselines reproducible
joshuakrueger-dfx Aug 7, 2026
e3a5a40
test: make the new guards say what they check
joshuakrueger-dfx Aug 7, 2026
8597b51
test(payment-link): cover the payment screen end to end
joshuakrueger-dfx Aug 8, 2026
43224e4
test(payment-routes): cover the routes screen and its form wizard
joshuakrueger-dfx Aug 8, 2026
582dad9
test: cover the routes QR label and the payment device split
joshuakrueger-dfx Aug 13, 2026
2d6dbc7
test(e2e-stack): click the default-config chevron and pin the large QR
joshuakrueger-dfx Aug 13, 2026
580279b
docs: declare the payment-link quote fake where the repo now keeps them
joshuakrueger-dfx Aug 13, 2026
6f0b6d1
fix(i18n): keep the Italian wallet noun as portafoglio after the rebase
joshuakrueger-dfx Aug 19, 2026
1d4ae39
test(payment-routes): pin which error the hint shows when both are set
joshuakrueger-dfx Aug 19, 2026
ba66758
test(e2e): drop the debug listeners from the device spec
joshuakrueger-dfx Aug 19, 2026
f1ea0f8
fix(i18n): address the Italian payer informally on the cashier line
joshuakrueger-dfx Aug 19, 2026
35f3d48
test(e2e): let an explicit REACT_APP_API_URL through to the dev server
joshuakrueger-dfx Aug 19, 2026
6f6b186
fix(payment-link): read the standard from the payment while the form …
joshuakrueger-dfx Aug 19, 2026
bb574e0
test(e2e): match the mocked API by path instead of by host
joshuakrueger-dfx Aug 19, 2026
aa80612
docs(test): declare what the two payment visual specs answer themselves
joshuakrueger-dfx Aug 19, 2026
cd3ed6a
docs(test): name the endpoints the routes-label spec actually answers
joshuakrueger-dfx Aug 19, 2026
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
17 changes: 17 additions & 0 deletions docs/test-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ run does not prove for each one; the taxonomy and cross-repository entries live
fixture with `{ userData: { verifiedName } }`. A green run proves that the review screen
accepts that name, not that the API returns the logged-in staff member's `verifiedName`.
The spec stays on the AML reset path and does not assert the Editor label.
- **The payment-link device-split tests answer the pay-request endpoint themselves.** The loc API
cannot build a Lightning/BTC transfer amount (`404 No BTC transfer amount found`), so
`e2e-stack/specs/payment-links.spec.ts` fulfils `paymentLink/payment` with a quoted OpenCryptoPay
payload (`displayQr: false`) and holds `lnurlp/wait` / `paymentLink/payment/wait` open. A green
run proves the desktop QR / handheld wallet-copy split for that payload, not that the loc API can
produce a quote or that a real quote's transfer amounts, expiry or callback match what the screen
then renders.

- **The two payment visual specs answer every API they need themselves.**
`e2e/payment-qr-device.spec.ts` fulfils the `/v1/` payment routes with a fixed quote whose
expiry is a constant ISO string, holds the wait-polling open so it never re-fetches, and pins
wall-clock time; `e2e/payment-routes-qr-label.spec.ts` answers `GET /v2/user`,
`GET /v1/route`, the payment-link list, its config and POS endpoints and the info banner with
static payloads, and mounts the screen on a synthetic unsigned JWT. A green
run of either proves that the screen renders those payloads — the device split, the wallet copy
and the renamed setting label — not that a real session passes the address guard, that the API
returns these shapes, or that a live quote expires when the baseline says it does.

## Known gaps

Expand Down
1 change: 1 addition & 0 deletions e2e-stack/docs/test-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ The self-reference check is deliberately conservative for a key spanning several
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Real bank/crypto **arrival** → auto process | Crons disabled (`DISABLED_PROCESSES=*`) |
| Live **price-based** payment infos | Outbound HTTP mocked; CoinGecko/etc. unreliable |
| Real Lightning/BTC **payment-link quote** | `createPayRequest` 404s with "No BTC transfer amount found". The two device-split `/pl` tests therefore fulfill `paymentLink/payment` themselves — see `docs/test-architecture.md`. A green run does not prove the loc API can quote a payment. |
| Full **Sumsub / IdNow** KYC completion | External ident providers mocked; use `createKycStep` + SQL `kycLevel` |
| Mail **verification codes** after first mail | Mail sending disabled; first `PUT /v2/user/mail` applies immediately when mail was null |
| Real **Lightning** payment-link API path | No Lightning deposits from EVM seed; factory uses SQL synthetic Lightning deposit |
Expand Down
167 changes: 166 additions & 1 deletion e2e-stack/specs/payment-links.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* SQL factory instead.
*/

import type { Page } from '@playwright/test';
import { devices, type Page, type Route } from '@playwright/test';
import {
apiGet,
cleanupCreatedData,
Expand All @@ -27,6 +27,70 @@ import {
waitForRow,
} from './fixtures';

const SCAN_COPY = 'Scan the QR-Code with a compatible app to complete the payment.';
const WALLET_COPY = 'Choose your wallet to open the payment.';

/**
* The loc API cannot build a Lightning/BTC transfer amount, so a quoted pay-request
* never reaches the browser (see docs/test-architecture.md). These two device-split tests
* replace only that response; everything else hits the real stack.
*/
async function installQuotedPayRequest(
page: Page,
opts: { merchant: string; amount: number; uniqueId: string },
): Promise<void> {
const quoted = {
id: opts.uniqueId,
externalId: `ext-${opts.uniqueId}`,
tag: 'payRequest',
displayName: opts.merchant,
standard: 'OpenCryptoPay',
possibleStandards: ['OpenCryptoPay'],
displayQr: false,
mode: 'Multiple',
route: 'e2e-quoted-route',
currency: 'CHF',
recipient: { name: opts.merchant },
transferAmounts: [
{
method: 'Lightning',
minFee: 0,
assets: [{ asset: 'BTC', amount: 0.00025 }],
available: true,
},
],
requestedAmount: { asset: 'CHF', amount: opts.amount },
quote: {
id: `q-${opts.uniqueId}`,
expiration: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
payment: `pay-${opts.uniqueId}`,
},
callback: 'https://api.example.test/v1/lnurlp/cb/quoted',
metadata: 'e2e-quoted',
minSendable: 1,
maxSendable: 100000000,
};

await page.route('**/v1/**', async (route: Route) => {
const url = route.request().url();
if (url.includes('/lnurlp/wait') || url.includes('paymentLink/payment/wait')) {
await new Promise(() => {
/* intentionally never resolves — same as the visual suite */
});
return;
}
if (url.includes('/paymentLink/payment')) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(quoted),
});
return;
}
await route.continue();
});
}

// ---------------------------------------------------------------------------
// Types & helpers
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -254,6 +318,11 @@ test.describe('Payment links / routes / invoice', () => {
// Payment Links section + card content unique to this screen.
await expect(page.getByRole('heading', { name: 'Payment Links', exact: true })).toBeVisible();
await expect(page.getByText('e2e-routes-pl-label', { exact: true })).toBeVisible();

// `displayQr` is a force switch; the merchant-facing label has to say so.
await page.getByText('Default configuration', { exact: true }).locator('xpath=following-sibling::div').click();
await expect(page.getByText('Always show QR code', { exact: true })).toBeVisible();
await expect(page.getByText('Display QR code', { exact: true })).toHaveCount(0);
if (pl.routeId) {
await expect(page.getByText(`Payment route ${pl.routeId}`, { exact: true })).toBeVisible();
}
Expand Down Expand Up @@ -352,6 +421,102 @@ test.describe('Payment links / routes / invoice', () => {
}
});

test.describe('device-aware /pl quote view', () => {
test.describe('desktop', () => {
test.use({
viewport: { width: 1280, height: 900 },
isMobile: false,
hasTouch: false,
});

test('/pl: desktop with a quote shows the large QR and the scan sentence', async ({ page }) => {
const user = await createUser({
tag: 'pl-qr-desktop',
language: 'EN',
kycLevel: 30,
completePersonalData: true,
});
const externalId = 'e2e-pl-qr-desktop';
const merchant = 'E2E Desktop Quoted Merchant';
const pl = await createPaymentLink(user.jwt, {
tag: 'pl-qr-desktop',
amount: 24,
label: 'e2e-pl-qr-desktop',
externalId,
});

await installQuotedPayRequest(page, {
merchant,
amount: 24,
uniqueId: pl.uniqueId,
});
await page.goto(
`/pl?routeId=${pl.routeId}&externalId=${encodeURIComponent(externalId)}&amount=24&currency=CHF`,
{ waitUntil: 'domcontentloaded' },
);
await expect
.poll(() => normPath(new URL(page.url()).pathname), {
message: 'expected pathname /pl',
timeout: 20000,
})
.toBe('/pl');

await expect(page.getByText(merchant, { exact: true })).toBeVisible({ timeout: 20000 });
await expect(page.getByText(SCAN_COPY, { exact: true })).toBeVisible();
await expect(page.getByText(WALLET_COPY, { exact: true })).toHaveCount(0);
await expect(page.locator('.w-48.my-3 svg')).toBeVisible();
});
});

test.describe('handheld', () => {
test.use({
userAgent: devices['iPhone 13'].userAgent,
viewport: devices['iPhone 13'].viewport,
deviceScaleFactor: devices['iPhone 13'].deviceScaleFactor,
isMobile: true,
hasTouch: true,
});

test('/pl: handheld with a quote shows wallet copy and the collapsed QR row', async ({ page }) => {
const user = await createUser({
tag: 'pl-qr-handheld',
language: 'EN',
kycLevel: 30,
completePersonalData: true,
});
const externalId = 'e2e-pl-qr-handheld';
const merchant = 'E2E Handheld Quoted Merchant';
const pl = await createPaymentLink(user.jwt, {
tag: 'pl-qr-handheld',
amount: 24,
label: 'e2e-pl-qr-handheld',
externalId,
});

await installQuotedPayRequest(page, {
merchant,
amount: 24,
uniqueId: pl.uniqueId,
});
await page.goto(
`/pl?routeId=${pl.routeId}&externalId=${encodeURIComponent(externalId)}&amount=24&currency=CHF`,
{ waitUntil: 'domcontentloaded' },
);
await expect
.poll(() => normPath(new URL(page.url()).pathname), {
message: 'expected pathname /pl',
timeout: 20000,
})
.toBe('/pl');

await expect(page.getByText(merchant, { exact: true })).toBeVisible({ timeout: 20000 });
await expect(page.getByText(WALLET_COPY, { exact: true })).toBeVisible();
await expect(page.getByText(SCAN_COPY, { exact: true })).toHaveCount(0);
await expect(page.locator('.w-48.my-3 svg')).toHaveCount(0);
});
});
});

test('/pl: the real lightning= URL from GET /paymentLink now resolves through the tests-container forwarder', async ({
page,
}) => {
Expand Down
Loading
Loading