diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..341a0b23 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,33 @@ +name: E2E tests +on: + push: + branches: [ main ] + pull_request: +jobs: + test: + 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: npx playwright install --with-deps + - name: Run Playwright tests + run: | + npm run start & + sleep 10 + npx playwright test + - 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/reverseSwap.spec.ts b/e2e/reverseSwap.spec.ts new file mode 100644 index 00000000..5f9dbe7e --- /dev/null +++ b/e2e/reverseSwap.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '@playwright/test'; +import { getBitcoinAddress, getBitcoinWalletTx, payInvoiceLnd } from "./utils"; + +test('Reverse swap BTC/BTC', async ({ page }) => { + await page.goto('https://localhost:5173'); + + 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/utils.ts b/e2e/utils.ts new file mode 100644 index 00000000..324b6004 --- /dev/null +++ b/e2e/utils.ts @@ -0,0 +1,34 @@ +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 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}`); +}; 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..e5babd8f 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,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..1911b0c4 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,83 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* 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: 'http://127.0.0.1:3000', + ignoreHTTPSErrors: true, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + headless: true, + trace: 'on-first-retry', + permissions: ["clipboard-read", "clipboard-write"], + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + /* gfy + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + */ + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); 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),