From 0fe7bcff6ef6f8b34a68162ccbc262adf845ea7e Mon Sep 17 00:00:00 2001 From: maybeast <78227110+maybeast@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:57:00 +0300 Subject: [PATCH] test: e2e tests for BTC swaps (#655) Co-authored-by: michael1011 --- .github/workflows/CI.yml | 69 ++++++++++++++++++++++++++------ .gitignore | 4 ++ .gitmodules | 3 ++ e2e/chainSwaps.spec.ts | 61 ++++++++++++++++++++++++++++ e2e/reverseSwap.spec.ts | 62 +++++++++++++++++++++++++++++ e2e/submarineSwap.spec.ts | 55 +++++++++++++++++++++++++ e2e/utils.ts | 67 +++++++++++++++++++++++++++++++ package-lock.json | 75 +++++++++++++++++++++++++++++++++-- package.json | 8 +++- playwright.config.ts | 57 ++++++++++++++++++++++++++ regtest | 1 + src/components/PayInvoice.tsx | 2 +- 12 files changed, 445 insertions(+), 19 deletions(-) create mode 100644 .gitmodules create mode 100644 e2e/chainSwaps.spec.ts create mode 100644 e2e/reverseSwap.spec.ts create mode 100644 e2e/submarineSwap.spec.ts create mode 100644 e2e/utils.ts create mode 100644 playwright.config.ts create mode 160000 regtest diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 20424ac0..0213927d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -3,16 +3,59 @@ name: CI on: [push, pull_request] jobs: - ci: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - run: npm ci - - run: npm run prettier-check - - run: npm run test - - run: npm run tsc + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - run: npm ci + + - run: npm run prettier-check + + - run: npm run test + + - run: npm run tsc + + e2e: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Start regtest + env: + COMPOSE_PROFILES: ci + run: | + git submodule init + git submodule update + chmod -R 777 regtest + cd regtest + ./start.sh + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npm run playwright:install + + - name: Run Playwright tests + env: + CI: true + run: npm run test:e2e + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 1742bb27..4a175b55 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ ts-out/ coverage/ node_modules/ public/config.json +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..f18645c2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "regtest"] + path = regtest + url = https://github.com/BoltzExchange/regtest.git diff --git a/e2e/chainSwaps.spec.ts b/e2e/chainSwaps.spec.ts new file mode 100644 index 00000000..8ba592f5 --- /dev/null +++ b/e2e/chainSwaps.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from "@playwright/test"; + +import { + elementsSendToAddress, + generateBitcoinBlock, + generateLiquidBlock, + getBitcoinAddress, +} from "./utils"; + +test.describe("Chain swap", () => { + test.beforeEach(async () => { + await generateBitcoinBlock(); + }); + + test("BTC/L-BTC", async ({ page }) => { + await page.goto("/"); + + const assetSelector = page.locator("div[class='asset asset-LN'] div"); + await assetSelector.click(); + + const lbtcAsset = page.locator("div[data-testid='select-L-BTC']"); + await lbtcAsset.click(); + + const receiveAmount = "0.01"; + const inputReceiveAmount = page.locator( + "input[data-testid='receiveAmount']", + ); + await inputReceiveAmount.fill(receiveAmount); + + const inputSendAmount = page.locator("input[data-testid='sendAmount']"); + const sendAmount = "0.0100168"; + await expect(inputSendAmount).toHaveValue(sendAmount); + + const inputOnchainAddress = page.locator( + "input[data-testid='onchainAddress']", + ); + await inputOnchainAddress.fill(await getBitcoinAddress()); + + const buttonCreateSwap = page.locator( + "button[data-testid='create-swap-button']", + ); + await buttonCreateSwap.click(); + + const skipDownload = page.getByText("Skip download"); + await skipDownload.click(); + + const buttons = page.locator("div[data-testid='pay-onchain-buttons']"); + const copyAddressButton = buttons.getByText("address"); + expect(copyAddressButton).toBeDefined(); + await copyAddressButton.click(); + + const sendAddress = await page.evaluate(() => { + return navigator.clipboard.readText(); + }); + expect(sendAddress).toBeDefined(); + + await elementsSendToAddress(sendAddress, sendAmount); + await generateLiquidBlock(); + // TODO: verify amounts + }); +}); diff --git a/e2e/reverseSwap.spec.ts b/e2e/reverseSwap.spec.ts new file mode 100644 index 00000000..cce23ba3 --- /dev/null +++ b/e2e/reverseSwap.spec.ts @@ -0,0 +1,62 @@ +import { expect, test } from "@playwright/test"; + +import { + generateBitcoinBlock, + getBitcoinAddress, + getBitcoinWalletTx, + payInvoiceLnd, +} from "./utils"; + +test.describe("reverseSwap", () => { + test.beforeEach(async () => { + await generateBitcoinBlock(); + }); + + test("Reverse swap BTC/BTC", async ({ page }) => { + await page.goto("/"); + + const receiveAmount = "0.01"; + const inputReceiveAmount = page.locator( + "input[data-testid='receiveAmount']", + ); + await inputReceiveAmount.fill(receiveAmount); + + const inputSendAmount = page.locator("input[data-testid='sendAmount']"); + await expect(inputSendAmount).toHaveValue("0.01005558"); + + const inputOnchainAddress = page.locator( + "input[data-testid='onchainAddress']", + ); + await inputOnchainAddress.fill(await getBitcoinAddress()); + + const buttonCreateSwap = page.locator( + "button[data-testid='create-swap-button']", + ); + await buttonCreateSwap.click(); + + const payInvoiceTitle = page.locator( + "h2[data-testid='pay-invoice-title']", + ); + await expect(payInvoiceTitle).toHaveText( + "Pay this invoice about 0.01005558 BTC", + ); + + const spanLightningInvoice = page.locator("span[class='btn']"); + await spanLightningInvoice.click(); + + const lightningInvoice = await page.evaluate(() => { + return navigator.clipboard.readText(); + }); + expect(lightningInvoice).toBeDefined(); + + await payInvoiceLnd(lightningInvoice); + + const txIdLink = page.getByText("open claim transaction"); + + const txId = (await txIdLink.getAttribute("href")).split("/").pop(); + expect(txId).toBeDefined(); + + const txInfo = JSON.parse(await getBitcoinWalletTx(txId)); + expect(txInfo.amount.toString()).toEqual(receiveAmount); + }); +}); diff --git a/e2e/submarineSwap.spec.ts b/e2e/submarineSwap.spec.ts new file mode 100644 index 00000000..c237da1f --- /dev/null +++ b/e2e/submarineSwap.spec.ts @@ -0,0 +1,55 @@ +import { expect, test } from "@playwright/test"; + +import { + bitcoinSendToAddress, + generateBitcoinBlock, + generateInvoiceLnd, +} from "./utils"; + +test.describe("Submarine swap", () => { + test.beforeEach(async () => { + await generateBitcoinBlock(); + }); + + test("Submarine swap BTC/BTC", async ({ page }) => { + await page.goto("/"); + + const divFlipAssets = page.locator("#flip-assets"); + await divFlipAssets.click(); + + const receiveAmount = "0.01"; + const inputReceiveAmount = page.locator( + "input[data-testid='receiveAmount']", + ); + await inputReceiveAmount.fill(receiveAmount); + + const inputSendAmount = page.locator("input[data-testid='sendAmount']"); + const sendAmount = "0.01005302"; + await expect(inputSendAmount).toHaveValue(sendAmount); + + const invoiceInput = page.locator("textarea[data-testid='invoice']"); + await invoiceInput.fill( + JSON.parse(await generateInvoiceLnd(1000000)).payment_request, + ); + const buttonCreateSwap = page.locator( + "button[data-testid='create-swap-button']", + ); + await buttonCreateSwap.click(); + + const skipDownload = page.getByText("Skip download"); + await skipDownload.click(); + + const copyAddressButton = page.getByText("address"); + expect(copyAddressButton).toBeDefined(); + await copyAddressButton.click(); + + const sendAddress = await page.evaluate(() => { + return navigator.clipboard.readText(); + }); + expect(sendAddress).toBeDefined(); + await bitcoinSendToAddress(sendAddress, sendAmount); + + await generateBitcoinBlock(); + // TODO: verify amounts + }); +}); diff --git a/e2e/utils.ts b/e2e/utils.ts new file mode 100644 index 00000000..15a5caa7 --- /dev/null +++ b/e2e/utils.ts @@ -0,0 +1,67 @@ +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); + +const executeInScriptsContainer = + 'docker exec boltz-scripts bash -c "source /etc/profile.d/utils.sh && '; + +const execCommand = async (command: string): Promise => { + try { + const { stdout, stderr } = await execAsync( + `${executeInScriptsContainer}${command}"`, + { shell: "/bin/bash" }, + ); + + if (stderr) { + throw new Error(`Error executing command: ${stderr}`); + } + + return stdout.trim(); + } catch (error) { + console.error(`Failed to execute command: ${command}`, error); + throw error; + } +}; + +export const getBitcoinAddress = async (): Promise => { + return execCommand("bitcoin-cli-sim-client getnewaddress"); +}; + +export const bitcoinSendToAddress = async ( + address: string, + amount: string, +): Promise => { + return execCommand( + `bitcoin-cli-sim-client sendtoaddress "${address}" ${amount}`, + ); +}; + +export const elementsSendToAddress = async ( + address: string, + amount: string, +): Promise => { + return execCommand( + `elements-cli-sim-client sendtoaddress "${address}" ${amount}`, + ); +}; + +export const generateBitcoinBlock = async (): Promise => { + return execCommand("bitcoin-cli-sim-client -generate"); +}; + +export const generateLiquidBlock = async (): Promise => { + return execCommand("elements-cli-sim-client -generate"); +}; + +export const getBitcoinWalletTx = async (txId: string): Promise => { + return execCommand(`bitcoin-cli-sim-client gettransaction ${txId}`); +}; + +export const payInvoiceLnd = async (invoice: string): Promise => { + return execCommand(`lncli-sim 1 payinvoice -f ${invoice}`); +}; + +export const generateInvoiceLnd = async (amount: number): Promise => { + return execCommand(`lncli-sim 1 addinvoice --amt ${amount}`); +}; diff --git a/package-lock.json b/package-lock.json index 12c5446f..a5477e50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,11 +38,13 @@ "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/preset-env": "^7.24.8", "@babel/preset-typescript": "^7.24.7", + "@playwright/test": "^1.45.3", "@solidjs/testing-library": "^0.8.9", "@testing-library/jest-dom": "^6.4.8", "@testing-library/user-event": "^14.5.2", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/jest": "^29.5.12", + "@types/node": "^22.0.2", "@webbtc/webln-types": "^3.0.0", "babel-jest": "^29.7.0", "babel-preset-jest": "^29.6.3", @@ -3543,6 +3545,21 @@ "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.2.tgz", "integrity": "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==" }, + "node_modules/@playwright/test": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz", + "integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==", + "dev": true, + "dependencies": { + "playwright": "1.45.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/plugin-inject": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", @@ -4262,9 +4279,12 @@ } }, "node_modules/@types/node": { - "version": "20.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz", - "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==" + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.2.tgz", + "integrity": "sha512-yPL6DyFwY5PiMVEwymNeqUTKsDczQBJ/5T7W/46RwLU/VH+AA8aT5TZkvBviLKLbbm0hlfftEkGrNzfRk/fofQ==", + "dependencies": { + "undici-types": "~6.11.1" + } }, "node_modules/@types/offscreencanvas": { "version": "2019.7.0", @@ -10420,6 +10440,50 @@ "node": ">=10" } }, + "node_modules/playwright": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz", + "integrity": "sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==", + "dev": true, + "dependencies": { + "playwright-core": "1.45.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.3.tgz", + "integrity": "sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -11796,6 +11860,11 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", + "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", diff --git a/package.json b/package.json index c597dce8..013d18fe 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "scripts": { "start": "npm run regtest && vite", "test": "jest --runInBand ./tests", + "test:e2e": "playwright test", + "playwright:install": "playwright install --with-deps chromium", "dev": "vite", "regtest": "cp src/configs/regtest.json public/config.json", "preview": "vite preview", @@ -12,8 +14,8 @@ "mainnet": "cp src/configs/mainnet.json public/config.json", "beta": "cp src/configs/beta.json public/config.json", "testnet": "cp src/configs/testnet.json public/config.json", - "prettier": "prettier --write src tests", - "prettier-check": "prettier --check src tests", + "prettier": "prettier --write src tests e2e", + "prettier-check": "prettier --check src tests e2e", "tsc": "tsc", "changelog": "git-cliff -o CHANGELOG.md" }, @@ -22,11 +24,13 @@ "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/preset-env": "^7.24.8", "@babel/preset-typescript": "^7.24.7", + "@playwright/test": "^1.45.3", "@solidjs/testing-library": "^0.8.9", "@testing-library/jest-dom": "^6.4.8", "@testing-library/user-event": "^14.5.2", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/jest": "^29.5.12", + "@types/node": "^22.0.2", "@webbtc/webln-types": "^3.0.0", "babel-jest": "^29.7.0", "babel-preset-jest": "^29.6.3", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..7b9e32f9 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,57 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./e2e", + /* Run tests in files in parallel */ + fullyParallel: false, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "https://localhost:5173", + ignoreHTTPSErrors: true, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + headless: true, + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + contextOptions: { + // chromium-specific permissions + permissions: ["clipboard-read", "clipboard-write"], + }, + }, + }, + + /* + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + */ + ], + + webServer: { + command: "npm run start", + port: 5173, + reuseExistingServer: !process.env.CI, + stdout: "pipe", + stderr: "pipe", + }, +}); diff --git a/regtest b/regtest new file mode 160000 index 00000000..c99113f1 --- /dev/null +++ b/regtest @@ -0,0 +1 @@ +Subproject commit c99113f13a3ca6acf7fd828856d6dcc99da526f2 diff --git a/src/components/PayInvoice.tsx b/src/components/PayInvoice.tsx index 35ac6365..08ef61d1 100644 --- a/src/components/PayInvoice.tsx +++ b/src/components/PayInvoice.tsx @@ -29,7 +29,7 @@ const PayInvoice = ({ return (
-

+

{t("pay_invoice_to", { amount: formatAmount( BigNumber(sendAmount),