Skip to content

Commit

Permalink
Add e2e test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
vyorkin committed Feb 13, 2025
1 parent 5068584 commit 40fa30f
Show file tree
Hide file tree
Showing 41 changed files with 3,409 additions and 16,278 deletions.
50 changes: 36 additions & 14 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ on:
pull_request:
types: [opened, synchronize]

env:
DEFI_SDK_API_URL: ${{ github.event.inputs.DEFI_SDK_API_URL || 'wss://api-v4.zerion.io/' }}
DEFI_SDK_TESTNET_API_URL: ${{ github.event.inputs.DEFI_SDK_TESTNET_API_URL || 'wss://api-testnet.zerion.io/' }}
ZERION_API_URL: ${{ github.event.inputs.ZERION_API_URL || 'https://zpi.zerion.io/' }}
ZERION_TESTNET_API_URL: ${{ github.event.inputs.ZERION_TESTNET_API_URL || 'https://zpi-testnet.zerion.io/' }}
BACKEND_ENV: ${{ github.event.inputs.BACKEND_ENV || '' }}
PROXY_URL: ${{ github.event.inputs.PROXY_URL || 'https://proxy.zerion.io/' }}
DEFI_SDK_TRANSACTIONS_API_URL: ${{ github.event.inputs.DEFI_SDK_TRANSACTIONS_API_URL || 'https://transactions.zerion.io' }}
DEFI_SDK_API_TOKEN: Zerion.0JOY6zZTTw6yl5Cvz9sdmXc7d5AhzVMG
SOCIAL_API_URL: ${{ github.event.inputs.SOCIAL_API_URL || 'https://social.zerion.io/' }}
TEST_WALLET_ADDRESS: ${{ secrets.TEST_WALLET_ADDRESS }}
FEATURE_SEND_FORM: on
FEATURE_FOOTER_BUG_BUTTON: off
MIXPANEL_TOKEN_PUBLIC: ${{ secrets.MIXPANEL_TOKEN_PUBLIC_DEV }}
FEATURE_LOYALTY_FLOW: on

jobs:
run_tests:
runs-on: ubuntu-latest
Expand All @@ -18,20 +34,26 @@ jobs:
with:
node-version: '20.x'

- name: Install dependencies and test
- name: Install dependencies and build
# TODO: Reuse env vars from ./pr.yml somehow
env:
DEFI_SDK_API_URL: ${{ github.event.inputs.DEFI_SDK_API_URL || 'wss://api-v4.zerion.io/' }}
ZERION_API_URL: ${{ github.event.inputs.ZERION_API_URL || 'https://zpi.zerion.io/' }}
BACKEND_ENV: ${{ github.event.inputs.BACKEND_ENV || '' }}
PROXY_URL: ${{ github.event.inputs.PROXY_URL || 'https://proxy.zerion.io/' }}
DEFI_SDK_TRANSACTIONS_API_URL: ${{ github.event.inputs.DEFI_SDK_TRANSACTIONS_API_URL || 'https://transactions.zerion.io' }}
DEFI_SDK_API_TOKEN: Zerion.0JOY6zZTTw6yl5Cvz9sdmXc7d5AhzVMG
SOCIAL_API_URL: ${{ github.event.inputs.SOCIAL_API_URL || 'https://social.zerion.io/' }}
TEST_WALLET_ADDRESS: ${{ secrets.TEST_WALLET_ADDRESS }}
FEATURE_SEND_FORM: on
FEATURE_FOOTER_BUG_BUTTON: off
MIXPANEL_TOKEN_PUBLIC: ${{ secrets.MIXPANEL_TOKEN_PUBLIC_DEV }}
run: |
npm install
npm test
npm run build:production
- name: Run unit tests
run: |
npm run test:unit
- name: Install Playwright browsers
run: npx playwright install --with-deps

- name: Run Playwright tests
run: xvfb-run npx playwright test

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: e2e-report
path: e2e-report/
retention-days: 30
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ dist
.env
.eslintcache
.parcel-cache
/test-results/
/e2e-report/
/blob-report/
/playwright/.cache/
30 changes: 30 additions & 0 deletions e2e/fixtures/components/erase-data-confirmation-dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect } from '@playwright/test';
import type { Locator } from '@playwright/test';

export class EraseDataConfirmationDialog {
readonly eraseButton = this.dialog.getByRole('button', {
name: /Erase My Data/i,
});
readonly backButton = this.dialog.getByRole('button', { name: /Back/i });
readonly eraseMyDataCheckbox = this.dialog.locator(
'label > input[type="checkbox"] + div'
);

constructor(public readonly dialog: Locator) {}

async matchesSnapshot() {
await expect(this.dialog).toMatchAriaSnapshot(`
- dialog:
- img
- text: Erase data for the browser extension? Your crypto assets remain secured on the blockchain and can be accessed with your private keys and recovery phrase Yes, erase my data
- checkbox "Yes, erase my data"
- button "Erase My Data"
- button "Back"
`);
}

async confirm() {
await this.eraseMyDataCheckbox.check();
await this.eraseButton.click();
}
}
35 changes: 35 additions & 0 deletions e2e/fixtures/components/verify-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect } from '@playwright/test';
import type { Page } from '@playwright/test';

interface Props {
text?: string;
buttonTitle?: string;
}

export class VerifyUserComponent {
readonly UNLOCK_BUTTON_TITLE = 'Unlock';

readonly passwordInput = this.page.getByPlaceholder('Enter password');
readonly unlockButton = this.page.getByRole('button', {
name: this.props?.buttonTitle ?? this.UNLOCK_BUTTON_TITLE,
});

constructor(public readonly page: Page, public readonly props?: Props) {}

async matchesSnapshot() {
await expect(this.page.locator('html')).toMatchAriaSnapshot(`
- document:
- navigation:
- button "Go back"
- text: Enter password
- text: Enter Password ${this.props?.text}
- textbox "Enter password"
- button "${this.props?.buttonTitle ?? this.UNLOCK_BUTTON_TITLE}"
`);
}

async unlock(password: string) {
await this.passwordInput.fill(password);
await this.unlockButton.click();
}
}
21 changes: 21 additions & 0 deletions e2e/fixtures/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { TestWallet } from './types';

export type TestWalletActor = 'alice' | 'bob' | 'charlie';

export const testWallets: Record<TestWalletActor, TestWallet> = {
alice: {
address: '0xf79C97988D930242AeEaA157D6b9d04fBB508fB8',
privateKey:
'0xc74db0fd6acd3c083125728af2801a6134f5e2c221c7bdf10b780d8b971467b1',
},
bob: {
address: '0x547B874AAD7Dd2BD8cc1c7D41adE96d18566aA16',
recoveryPhrase:
'tower sun allow merry shrug neutral number axis olympic taxi orange animal',
},
charlie: {
address: '0x937FaA3594740e719c2B5468fE0FA3380D41666a',
privateKey:
'0x755d3a4465eefcfb22a2167ad23a5376083c5e20ff70c8c231d4650f1a5fa6f0',
},
};
25 changes: 25 additions & 0 deletions e2e/fixtures/interactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Locator } from '@playwright/test';

export async function selectItems({
items,
count,
showMoreLocator,
continueLocator,
}: {
items: Locator;
count: number;
showMoreLocator: Locator;
continueLocator?: Locator;
}) {
while ((await items.count()) < count) {
await showMoreLocator.click();
}

for (let i = 0; i < count; i++) {
await items.nth(i).click();
}

if (continueLocator) {
await continueLocator.click();
}
}
17 changes: 17 additions & 0 deletions e2e/fixtures/pages/connect-ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect } from '@playwright/test';
import { ExtensionPage } from './extension-page';

export class ConnectLedgerPage extends ExtensionPage {
readonly connectButton = this.page.getByRole('button', {});

async matchesSnapshot() {
await expect(this.page.locator('html')).toMatchAriaSnapshot(`
- document:
- img
- button "Go back"
- contentinfo:
- text: We never store your keys. Please find more details in our
- link "Privacy Policy."
`);
}
}
28 changes: 28 additions & 0 deletions e2e/fixtures/pages/extension-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Page } from '@playwright/test';
import type { ExtensionPopupUrl } from 'e2e/tests/test';
import { setUrlContext } from 'src/shared/setUrlContext';
import type { UrlContext } from 'src/shared/types/UrlContext';

interface Params {
extensionPopupUrl: ExtensionPopupUrl;
path: string;
urlContext?: Partial<UrlContext>;
}

export abstract class ExtensionPage {
page: Page;
url: URL;

constructor(page: Page, { extensionPopupUrl, path, urlContext }: Params) {
const url = new URL(`#${path}`, extensionPopupUrl);
if (urlContext) {
setUrlContext(url.searchParams, urlContext);
}
this.page = page;
this.url = url;
}

async navigateTo() {
await this.page.goto(this.url.toString());
}
}
28 changes: 28 additions & 0 deletions e2e/fixtures/pages/forgot-password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect } from '@playwright/test';
import { ExtensionPage } from './extension-page';

export class ForgotPasswordPage extends ExtensionPage {
readonly tryPasswordAgainLink = this.page.getByRole('link', {
name: /Try Password Again/i,
});
readonly clearAllDataButton = this.page.getByRole('button', {
name: /Clear All Data/i,
});

async matchesSnapshot() {
await expect(this.page.locator('html')).toMatchAriaSnapshot(`
- document:
- navigation:
- button "Go back"
- text: Forgot Password
- heading "Forgot your password?" [level=1]
- list:
- listitem:
- strong: We’re unable to recover the password
- text: for you because it’s stored securely and locally only on your computer. Try entering the correct password again.
- listitem: Alternatively, you can create a new account and password by deleting your data and import your wallets again with the recovery phrase or private keys.
- link "Try Password Again"
- button "Clear All Data"
`);
}
}
116 changes: 116 additions & 0 deletions e2e/fixtures/pages/get-started/get-started.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { selectItems } from 'e2e/fixtures/interactions';
import { ExtensionPage } from '../extension-page';

export class GetStartedPage extends ExtensionPage {
readonly createNewWalletLink = this.page.getByRole('link', {
name: /Create New Wallet/i,
});
readonly addExistingWalletLink = this.page.getByRole('link', {
name: /Add Existing Wallet/i,
});

async matchesSnapshot() {
await expect(this.page.locator('html')).toMatchAriaSnapshot(`
- document:
- navigation:
- button "Go back"
- img
- text: Add Wallet Choose an option to set up your wallet
- link "Create New Wallet"
- link "Add Existing Wallet"
`);
}
}

export class WalletGroupSelectPage {
readonly walletGroupsLinks = this.page.getByRole('link', {
name: /\.*0x\.*/,
});

readonly createNewBackupLink = this.page.getByRole('link', {
name: /Create New Backup/i,
});

constructor(public readonly page: Page) {}

async matchesSnapshot() {
await expect(this.page.locator('html')).toMatchAriaSnapshot(`
- document:
- navigation:
- button "Go back"
- text: Select Backup
- 'link "Wallet Group #1 0x547B…aA16"':
- img
- link "Create New Backup":
- img
`);
}

async selectBackup(n: number) {
await this.walletGroupsLinks.nth(n).click();
}
}

export class MnemonicImportView {
constructor(public readonly page: Page) {}

async matchesSnapshot() {
await expect(this.page.locator('html')).toMatchAriaSnapshot(`
- document:
- navigation:
- button "Go back"
- text: Wallets Ready to Import
- text: We found these wallets associated with your recovery phrase Inactive wallets
- button "0 0x547B8…6aA16 Already added"
- button "1 0x00Be4…F6AB6":
- img
- button "2 0xAfD09…96Cc6":
- img
- button "Show More"
- button "Continue" [disabled]
`);
}

async selectWallets(count: number) {
await selectItems({
items: this.page
.getByRole('button', { name: /0x\.*/ })
.filter({ hasNotText: /Already added/i }),
count,
showMoreLocator: this.page.getByRole('button', { name: /Show More/i }),
continueLocator: this.page.getByRole('button', { name: /Continue\.*/i }),
});
}
}

export class AddressImportMessagesView {
constructor(public readonly page: Page) {}

async ready() {
await expect(
this.page.getByRole('link', { name: /Finish/i })
).toBeInViewport();
}

async matchesSnapshot() {
await expect(this.page.locator('html')).toMatchAriaSnapshot(`
- document:
- navigation:
- button "Go back"
- text: Wallets Ready to Import
- img
- text: ⏳ Checking your wallet history on the blockchain... 🔐 Encrypting your wallet with your password...
- img
- text: All done! Your wallets have been imported 🚀
- img
- text: Congrats! Welcome on board 0x00Be4693…80CF6AB6 0xAfD09a42…Bcd96Cc6 0x6F3238DB…f05f529b 0x9B4B3Ae9…e3e816C2
- link "Finish"
`);
}

async finish() {
await this.page.getByRole('link', { name: /Finish/i }).click();
}
}
Loading

0 comments on commit 40fa30f

Please sign in to comment.