diff --git a/.github/labeler.yml b/.github/labeler.yml index 02e1481b..fc855b1e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -29,6 +29,7 @@ - any: - changed-files: - any-glob-to-any-file: '**/*.test.ts' + - any-glob-to-any-file: 'tests/**/*' 'area: i18n': - any: diff --git a/.github/workflows/nightly-build.yaml b/.github/workflows/nightly-build.yaml index 1bbbe384..56870341 100644 --- a/.github/workflows/nightly-build.yaml +++ b/.github/workflows/nightly-build.yaml @@ -14,9 +14,67 @@ defaults: shell: bash jobs: + test-e2e: + strategy: + fail-fast: false + matrix: + include: + - name: Chrome + project: chrome + target: chrome + runs-on: ubuntu-22.04 + - name: Edge + project: msedge + target: chrome + runs-on: ubuntu-22.04 + timeout-minutes: 15 + name: E2E Tests - ${{ matrix.name }} + runs-on: ${{ matrix.runs-on }} + environment: test + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Environment setup + uses: ./.github/actions/setup + + - name: Build + run: pnpm build ${{ matrix.target }} --channel=nightly + + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + + - name: Run Playwright tests + run: xvfb-run pnpm test:e2e:${{ matrix.project }} + env: + PLAYWRIGHT_PROJECT: ${{ matrix.project }} + PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS: '1' + WALLET_URL_ORIGIN: ${{ vars.E2E_WALLET_URL_ORIGIN }} + WALLET_USERNAME: ${{ vars.E2E_WALLET_USERNAME }} + WALLET_PASSWORD: ${{ secrets.E2E_WALLET_PASSWORD }} + CONNECT_WALLET_ADDRESS_URL: ${{ vars.E2E_CONNECT_WALLET_ADDRESS_URL }} + CONNECT_KEY_ID: ${{ vars.E2E_CONNECT_KEY_ID }} + CONNECT_PUBLIC_KEY: ${{ secrets.E2E_CONNECT_PUBLIC_KEY }} + CONNECT_PRIVATE_KEY: ${{ secrets.E2E_CONNECT_PRIVATE_KEY }} + + - name: Encrypt report + shell: bash + working-directory: tests/e2e/playwright-report + run: | + zip -r -P ${{ secrets.E2E_PLAYWRIGHT_REPORT_PASSWORD }} ../playwright-report.zip * + + - name: Upload report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.project }} + path: tests/e2e/playwright-report.zip + retention-days: 3 + build-nightly: name: Create Release runs-on: ubuntu-22.04 + needs: test-e2e steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/tests-e2e.yml b/.github/workflows/tests-e2e.yml new file mode 100644 index 00000000..33763569 --- /dev/null +++ b/.github/workflows/tests-e2e.yml @@ -0,0 +1,74 @@ +name: End-to-End Tests +on: + pull_request_review: + types: [submitted] + +concurrency: + group: ${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + test-e2e: + if: ${{ + github.event.review.body == 'test-e2e' && + contains(fromJson('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association) + }} + strategy: + fail-fast: false + matrix: + include: + - name: Chrome + project: chrome + target: chrome + runs-on: ubuntu-22.04 + # - name: Firefox + # project: firefox + # target: firefox + # runs-on: ubuntu-22.04 + - name: Edge + project: msedge + target: chrome + runs-on: ubuntu-22.04 + + timeout-minutes: 15 + name: E2E Tests - ${{ matrix.name }} + runs-on: ${{ matrix.runs-on }} + environment: test + steps: + - uses: actions/checkout@v4 + + - name: Environment setup + uses: ./.github/actions/setup + + - name: Build + run: pnpm build ${{ matrix.target }} --channel=nightly + + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + + - name: Run Playwright tests + run: xvfb-run pnpm test:e2e:${{ matrix.project }} + env: + PLAYWRIGHT_PROJECT: ${{ matrix.project }} + PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS: '1' + WALLET_URL_ORIGIN: ${{ vars.E2E_WALLET_URL_ORIGIN }} + WALLET_USERNAME: ${{ vars.E2E_WALLET_USERNAME }} + WALLET_PASSWORD: ${{ secrets.E2E_WALLET_PASSWORD }} + CONNECT_WALLET_ADDRESS_URL: ${{ vars.E2E_CONNECT_WALLET_ADDRESS_URL }} + CONNECT_KEY_ID: ${{ vars.E2E_CONNECT_KEY_ID }} + CONNECT_PUBLIC_KEY: ${{ secrets.E2E_CONNECT_PUBLIC_KEY }} + CONNECT_PRIVATE_KEY: ${{ secrets.E2E_CONNECT_PRIVATE_KEY }} + + - name: Encrypt report + shell: bash + working-directory: tests/e2e/playwright-report + run: | + zip -r -P ${{ secrets.E2E_PLAYWRIGHT_REPORT_PASSWORD }} ../playwright-report.zip * + + - name: Upload report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.project }} + path: tests/e2e/playwright-report.zip + retention-days: 3 diff --git a/.gitignore b/.gitignore index af7f9d5e..756d299c 100755 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ dist-firefox-v2 public/manifest.json *.local coverage +.env .husky .vscode @@ -30,3 +31,9 @@ coverage *.njsproj *.sln *.sw? + +# playwright +/tests/e2e/test-results/ +/tests/e2e/test-results/.auth +/tests/e2e/playwright-report/ +/playwright/.cache/ diff --git a/cspell-dictionary.txt b/cspell-dictionary.txt index 68d1a910..7ce98d16 100644 --- a/cspell-dictionary.txt +++ b/cspell-dictionary.txt @@ -27,6 +27,10 @@ iife backported autobuild buildscript +networkidle +webextensions +firefoxUserPrefs +textbox # packages and 3rd party tools/libraries awilix @@ -35,6 +39,8 @@ loglevel openapi apidevtools tailwindcss +msedge +xvfb # user names raducristianpopa @@ -43,3 +49,4 @@ dianafulga jgoz amannn softprops +Gidarakos diff --git a/cspell.json b/cspell.json index 503ba166..76c28f53 100644 --- a/cspell.json +++ b/cspell.json @@ -18,6 +18,7 @@ "*.svg", "pnpm-lock.yaml", ".eslintrc.json", + ".gitignore", "cspell-dictionary.txt" ] } diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 00000000..8bb9f5d6 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,56 @@ +# Automated Testing + +## Unit tests + +Run `pnpm test` to run unit tests locally. These tests are run automatically on every pull request. + +## End-to-end Tests + +To run end-to-end tests with chromium, run `pnpm test:e2e` in terminal. + +**Before you begin**, you need to setup some environment variables/secrets in `tests/.env`. + +1. Copy `tests/.env.example` to `tests/.env` +2. Update `tests/.env` with your secrets. + +| Environment Variable | Description | Is secret? | +| ---------------------------- | ----------------------------------------------------------- | ---------- | +| `WALLET_URL_ORIGIN` | URL of the wallet (e.g. https://rafiki.money) | No | +| `WALLET_USERNAME` | Login email for the wallet | No | +| `WALLET_PASSWORD` | Login password for the wallet | Yes | +| `CONNECT_WALLET_ADDRESS_URL` | Your wallet address that will be connected to extension | No | +| `CONNECT_KEY_ID` | ID of the key that will be connected to extension (UUID v4) | No | +| `CONNECT_PRIVATE_KEY` | Private key (hex-encoded Ed25519 private key) | Yes | +| `CONNECT_PUBLIC_KEY` | Public key (base64-encoded Ed25519 public key) | No | + +To get the `CONNECT_KEY_ID`, `CONNECT_PRIVATE_KEY` and `CONNECT_PUBLIC_KEY`: + +1. Load the extension in browser (via `chrome://extensions/`) + - Once the extension is loaded, it'll generate a key-pair that we will need to connect with our wallet. +1. Inspect service worker with "Inspect views service worker" +1. Run following in devtools console to copy keys to your clipboard, and paste it in `tests/.env`: + ```js + // 1. Gets generated keys from extension storage. + // 2. Converts result to `CONNECT_{X}="VAL"` format for use in .env file. + // 3. Copies result to clipboard. + copy( + Object.entries( + await chrome.storage.local.get(['privateKey', 'publicKey', 'keyId']), + ) + .map( + ([k, v]) => + `CONNECT_${k.replace(/([A-Z])/g, '_$1').toUpperCase()}="${v}"`, + ) + .join('\n'), + ); + ``` +1. Then copy `CONNECT_PUBLIC_KEY` key to https://rafiki.money/settings/developer-keys under your wallet address. +1. Now you're ready to run the tests. + +### How to run in end-to-end tests in GitHub + +As these tests are expensive/time-consuming, these need to be triggered manually when needed, instead of on every pull request/commit. + +For a pull request, users with write access to repository can trigger the workflow to run end-to-end tests by adding a review-comment (from PR Files tab) with body `test-e2e` (exactly). + +End-to-end tests run automatically daily before creating the Nightly release. You can also trigger that workflow manually from [Actions Dashboard](https://github.com/interledger/web-monetization-extension/actions/workflows/nightly-build.yaml). diff --git a/jest.config.ts b/jest.config.ts index ef369982..e625acaf 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -21,6 +21,7 @@ export default { testEnvironment: 'jsdom', testPathIgnorePatterns: [ '/node_modules/', + '/tests/', '/jest.config.ts', ], transform: { diff --git a/package.json b/package.json index b725fb2b..005f2605 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,9 @@ "format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/prettiercache' --log-level=warn", "typecheck": "tsc --noEmit", "test": "jest --maxWorkers=2 --passWithNoTests", + "test:e2e": "pnpm test:e2e:chrome", + "test:e2e:chrome": "playwright test --project=chrome", + "test:e2e:msedge": "playwright test --project=msedge", "test:ci": "pnpm test -- --reporters=default --reporters=github-actions" }, "dependencies": { @@ -42,10 +45,12 @@ }, "devDependencies": { "@jgoz/esbuild-plugin-typecheck": "^4.0.1", + "@playwright/test": "^1.47.0", "@tailwindcss/forms": "^0.5.7", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@types/archiver": "^6.0.2", + "@types/chrome": "^0.0.270", "@types/github-script": "github:actions/github-script", "@types/jest": "^29.5.12", "@types/node": "^20.16.2", @@ -57,6 +62,7 @@ "@typescript-eslint/parser": "^8.5.0", "archiver": "^7.0.1", "autoprefixer": "^10.4.20", + "dotenv": "^16.4.5", "esbuild": "^0.23.1", "esbuild-node-builtin": "^0.1.1", "esbuild-plugin-copy": "^2.1.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..6d0c9ebd --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import path from 'node:path'; +import { defineConfig, devices } from '@playwright/test'; +import { testDir, authFile } from './tests/e2e/fixtures/helpers'; + +if (!process.env.CI) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + require('dotenv').config({ path: path.join(testDir, '.env') }); +} + +export default defineConfig({ + testDir, + outputDir: path.join(testDir, 'test-results'), + // We don't want this set to true as that would make tests in each file to run + // in parallel, which will cause conflicts with the "global state". With this + // set to false and workers > 1, multiple test files can run in parallel, but + // tests within a file are run at one at a time. We make extensive use of + // worker-scope fixtures and beforeAll hooks to achieve best performance. + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['list'], + [ + 'html', + { open: 'never', outputFolder: path.join(testDir, 'playwright-report') }, + ], + ], + use: { trace: 'on-first-retry' }, + + projects: [ + { + name: 'setup', + testMatch: /.*\.setup\.ts/, + }, + + { + name: 'chrome', + use: { + ...devices['Desktop Chrome'], + storageState: authFile, + channel: 'chrome', + }, + dependencies: ['setup'], + }, + + // Firefox+Playwright doesn't work well enough at the moment. + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'], storageState: authFile }, + // dependencies: ['setup'], + // }, + + // Safari is surely a no-go for now + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'], storageState: authFile }, + // dependencies: ['setup'], + // }, + + { + name: 'msedge', + use: { + ...devices['Desktop Edge'], + channel: 'msedge', + storageState: authFile, + }, + dependencies: ['setup'], + }, + ], + + /* 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/pnpm-lock.yaml b/pnpm-lock.yaml index 7be0cbc1..fc3d44a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,6 +76,9 @@ importers: '@jgoz/esbuild-plugin-typecheck': specifier: ^4.0.1 version: 4.0.1(esbuild@0.23.1)(typescript@5.6.2) + '@playwright/test': + specifier: ^1.47.0 + version: 1.47.0 '@tailwindcss/forms': specifier: ^0.5.7 version: 0.5.7(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@20.16.2)(typescript@5.6.2))) @@ -88,6 +91,9 @@ importers: '@types/archiver': specifier: ^6.0.2 version: 6.0.2 + '@types/chrome': + specifier: ^0.0.270 + version: 0.0.270 '@types/github-script': specifier: github:actions/github-script version: github-script@https://codeload.github.com/actions/github-script/tar.gz/35b1cdd1b2c1fc704b1cd9758d10f67e833fcb02 @@ -121,6 +127,9 @@ importers: autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.41) + dotenv: + specifier: ^16.4.5 + version: 16.4.5 esbuild: specifier: ^0.23.1 version: 0.23.1 @@ -795,6 +804,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.47.0': + resolution: {integrity: sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==} + engines: {node: '>=18'} + hasBin: true + '@remix-run/router@1.19.1': resolution: {integrity: sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==} engines: {node: '>=14.0.0'} @@ -971,6 +985,9 @@ packages: '@types/chrome@0.0.114': resolution: {integrity: sha512-i7qRr74IrxHtbnrZSKUuP5Uvd5EOKwlwJq/yp7+yTPihOXnPhNQO4Z5bqb1XTnrjdbUKEJicaVVbhcgtRijmLA==} + '@types/chrome@0.0.270': + resolution: {integrity: sha512-ADvkowV7YnJfycZZxL2brluZ6STGW+9oKG37B422UePf2PCXuFA/XdERI0T18wtuWPx0tmFeZqq6MOXVk1IC+Q==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -1758,6 +1775,10 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2079,6 +2100,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3089,6 +3115,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.47.0: + resolution: {integrity: sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.47.0: + resolution: {integrity: sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -4673,6 +4709,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.47.0': + dependencies: + playwright: 1.47.0 + '@remix-run/router@1.19.1': {} '@rollup/plugin-inject@5.0.5(rollup@4.20.0)': @@ -4831,6 +4871,11 @@ snapshots: '@types/filesystem': 0.0.35 '@types/har-format': 1.2.15 + '@types/chrome@0.0.270': + dependencies: + '@types/filesystem': 0.0.35 + '@types/har-format': 1.2.15 + '@types/estree@1.0.5': {} '@types/filesystem@0.0.35': @@ -5766,6 +5811,8 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv@16.4.5: {} + eastasianwidth@0.2.0: {} ee-first@1.1.1: {} @@ -6256,6 +6303,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -7506,6 +7556,14 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.47.0: {} + + playwright@1.47.0: + dependencies: + playwright-core: 1.47.0 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.0.0: {} postcss-import@15.1.0(postcss@8.4.41): diff --git a/src/popup/components/ConnectWalletForm.tsx b/src/popup/components/ConnectWalletForm.tsx index 32248ae6..db48647b 100644 --- a/src/popup/components/ConnectWalletForm.tsx +++ b/src/popup/components/ConnectWalletForm.tsx @@ -87,6 +87,7 @@ export const ConnectWalletForm = ({ return (
{ const response = await connectWallet({ ...data, diff --git a/src/popup/components/WalletInformation.tsx b/src/popup/components/WalletInformation.tsx index 7cc85069..5f302f7d 100644 --- a/src/popup/components/WalletInformation.tsx +++ b/src/popup/components/WalletInformation.tsx @@ -45,7 +45,7 @@ export const WalletInformation = ({ info }: WalletInformationProps) => { type="submit" variant="destructive" className="w-full" - aria-label="Connect your wallet" + aria-label="Disconnect your wallet" disabled={isSubmitting} loading={isSubmitting} > diff --git a/src/popup/components/layout/MainLayout.tsx b/src/popup/components/layout/MainLayout.tsx index 4f8174b5..5b5c3650 100644 --- a/src/popup/components/layout/MainLayout.tsx +++ b/src/popup/components/layout/MainLayout.tsx @@ -9,7 +9,10 @@ const Divider = () => { export const MainLayout = () => { return ( -
+
diff --git a/src/popup/pages/Home.tsx b/src/popup/pages/Home.tsx index adcc5249..908b0682 100644 --- a/src/popup/pages/Home.tsx +++ b/src/popup/pages/Home.tsx @@ -74,7 +74,7 @@ export const Component = () => { } return ( -
+
{enabled ? (