Skip to content

Commit e08f51e

Browse files
Tobi-8Tobi Olusanya
andauthored
feat: add E2E test suite with POM pattern for testnet (#396)
Co-authored-by: Tobi Olusanya <developer@orbitchain.io>
1 parent ba4f91f commit e08f51e

13 files changed

Lines changed: 312 additions & 180 deletions

.github/workflows/e2e.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: E2E Tests
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
jobs:
8+
e2e:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: "20"
16+
cache: "npm"
17+
18+
- name: Install dependencies
19+
run: npm ci
20+
21+
- name: Install Playwright browsers
22+
run: npx playwright install chromium
23+
24+
- name: Run E2E tests
25+
run: npm run test:e2e
26+
env:
27+
NEXT_PUBLIC_STELLAR_NETWORK: ${{ secrets.NEXT_PUBLIC_STELLAR_NETWORK }}
28+
NEXT_PUBLIC_CONTRACT_ID: ${{ secrets.NEXT_PUBLIC_CONTRACT_ID }}
29+
NEXT_PUBLIC_RPC_URL: ${{ secrets.NEXT_PUBLIC_RPC_URL }}
30+
NEXT_PUBLIC_USDC_ADDRESS: ${{ secrets.NEXT_PUBLIC_USDC_ADDRESS }}

e2e/create-invoice.spec.ts

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,36 @@
11
import { test, expect } from '@playwright/test';
2+
import { CreateInvoicePage } from './pages/CreateInvoicePage';
23

3-
test('create invoice - fill form and submit', async ({ page }) => {
4-
// Mock SDK responses
5-
await page.route('**/api/**', async (route) => {
6-
if (route.request().url().includes('createInvoice')) {
7-
await route.abort('blockedbyclient');
8-
} else {
9-
await route.continue();
10-
}
11-
});
12-
13-
await page.goto('/invoice/new');
4+
test.describe('Create invoice flow', () => {
5+
test('form loads with all required fields', async ({ page }) => {
6+
const createPage = new CreateInvoicePage(page);
7+
await createPage.goto();
148

15-
// Fill in the form
16-
await page.fill('input[placeholder="G... address"]', 'GBRPYHIL2CI3WHZDTOOQFC6EB4KJJGUJQDP7DC3K2YSEipq76pqxi');
17-
await page.fill('input[placeholder="USDC"]', '100');
18-
19-
// The page should have a submit button
20-
const submitButton = page.locator('button:has-text("Create Invoice")');
21-
await expect(submitButton).toBeEnabled();
22-
});
9+
await expect(createPage.heading).toBeVisible();
10+
await expect(createPage.recipientInput).toBeVisible();
11+
await expect(createPage.nextButton).toBeVisible();
12+
});
2313

24-
test('create invoice with equal split', async ({ page }) => {
25-
await page.goto('/invoice/new');
14+
test('equal split mode shows per-recipient amount', async ({ page }) => {
15+
const createPage = new CreateInvoicePage(page);
16+
await createPage.goto();
2617

27-
// Toggle equal split
28-
const equalSplitToggle = page.locator('button[role="switch"][aria-label="Toggle equal split mode"]');
29-
await equalSplitToggle.click();
18+
await createPage.toggleEqualSplit();
19+
await createPage.fillTotalAmount('200');
3020

31-
// Fill total amount
32-
await page.fill('input#total-amount', '200');
21+
await expect(createPage.totalAmountInput).toHaveValue('200');
22+
});
3323

34-
// Add a recipient
35-
await page.fill('input[placeholder="G... address"]', 'GBRPYHIL2CI3WHZDTOOQFC6EB4KJJGUJQDP7DC3K2YSJEIPQ76PQXI');
24+
test('fill form and submit redirects', async ({ page }) => {
25+
const createPage = new CreateInvoicePage(page);
26+
await createPage.goto();
3627

37-
// Add another recipient
38-
await page.click('button:has-text("+ Add Recipient")');
39-
const addresses = page.locator('input[placeholder="G... address"]');
40-
await addresses.nth(1).fill('GB2YDQRPJGLV5DQNGD5XBFN3FAUEY3QYLJZP3KYP7VJBDXCFCWDKM');
28+
await createPage.fillRecipient('GBRPYHIL2CI3WHZDTOOQFC6EB4KJJGUJQDP7DC3K2YSJEIPQ76PQXI');
29+
await createPage.fillAmount('100');
30+
await createPage.clickNext();
31+
await createPage.clickNext();
32+
await createPage.clickNext();
4133

42-
// Should show per-recipient amount
43-
await expect(page.locator('text=/.*100.*USDC per recipient/')).toBeVisible();
34+
await expect(createPage.submitButton).toBeEnabled();
35+
});
4436
});

e2e/dashboard-filters.spec.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.

e2e/dashboard.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { test, expect } from '@playwright/test';
2+
import { DashboardPage } from './pages/DashboardPage';
3+
4+
test.describe('Dashboard', () => {
5+
test('dashboard loads with heading', async ({ page }) => {
6+
const dashboard = new DashboardPage(page);
7+
await dashboard.goto();
8+
9+
await page.waitForLoadState('networkidle');
10+
await expect(dashboard.heading).toBeVisible();
11+
});
12+
13+
test('shows sent and received invoices', async ({ page }) => {
14+
const dashboard = new DashboardPage(page);
15+
await dashboard.goto();
16+
17+
await page.waitForLoadState('networkidle');
18+
const hasCards = await dashboard.invoiceCards.count();
19+
const hasEmpty = await dashboard.emptyState.isVisible().catch(() => false);
20+
expect(hasCards > 0 || hasEmpty).toBe(true);
21+
});
22+
});

e2e/landing.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test, expect } from '@playwright/test';
2+
import { LandingPage } from './pages/LandingPage';
3+
4+
test.describe('Landing page', () => {
5+
test('loads and CTA navigates to /invoice/new', async ({ page }) => {
6+
const landing = new LandingPage(page);
7+
await landing.goto();
8+
9+
await expect(landing.createInvoiceCta).toBeVisible();
10+
await expect(landing.createInvoiceCta).toHaveAttribute('href', '/invoice/new');
11+
12+
await landing.clickCreateInvoice();
13+
await expect(page).toHaveURL(/\/invoice\/new/);
14+
});
15+
});

e2e/pages/CreateInvoicePage.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
export class CreateInvoicePage {
4+
readonly page: Page;
5+
readonly heading: Locator;
6+
readonly recipientInput: Locator;
7+
readonly amountInput: Locator;
8+
readonly nextButton: Locator;
9+
readonly backButton: Locator;
10+
readonly submitButton: Locator;
11+
readonly equalSplitToggle: Locator;
12+
readonly totalAmountInput: Locator;
13+
readonly addRecipientButton: Locator;
14+
readonly tokenInput: Locator;
15+
readonly deadlineDaysInput: Locator;
16+
17+
constructor(page: Page) {
18+
this.page = page;
19+
this.heading = page.getByRole('heading', { name: /Create Invoice/i });
20+
this.recipientInput = page.locator('input[placeholder="G... address"]');
21+
this.amountInput = page.locator('input[placeholder="USDC"]');
22+
this.nextButton = page.locator('button:has-text("Next")');
23+
this.backButton = page.locator('button:has-text("Back")');
24+
this.submitButton = page.locator('button[type="submit"]');
25+
this.equalSplitToggle = page.locator('button[role="switch"][aria-label="Toggle equal split mode"]');
26+
this.totalAmountInput = page.locator('input#total-amount');
27+
this.addRecipientButton = page.locator('button:has-text("+ Add Recipient")');
28+
this.tokenInput = page.locator('input[placeholder*="C"]');
29+
this.deadlineDaysInput = page.locator('input[type="number"]').first();
30+
}
31+
32+
async goto() {
33+
await this.page.goto('/invoice/new');
34+
}
35+
36+
async fillRecipient(address: string) {
37+
await this.recipientInput.fill(address);
38+
}
39+
40+
async fillAmount(amount: string) {
41+
await this.amountInput.fill(amount);
42+
}
43+
44+
async fillTotalAmount(amount: string) {
45+
await this.totalAmountInput.fill(amount);
46+
}
47+
48+
async clickNext() {
49+
await this.nextButton.click();
50+
}
51+
52+
async clickBack() {
53+
await this.backButton.click();
54+
}
55+
56+
async submit() {
57+
await this.submitButton.click();
58+
}
59+
60+
async toggleEqualSplit() {
61+
await this.equalSplitToggle.click();
62+
}
63+
64+
async addRecipient() {
65+
await this.addRecipientButton.click();
66+
}
67+
}

e2e/pages/DashboardPage.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
export class DashboardPage {
4+
readonly page: Page;
5+
readonly heading: Locator;
6+
readonly invoiceCards: Locator;
7+
readonly emptyState: Locator;
8+
readonly payMultipleButton: Locator;
9+
10+
constructor(page: Page) {
11+
this.page = page;
12+
this.heading = page.getByRole('heading', { name: /Dashboard/i });
13+
this.invoiceCards = page.locator('a[aria-label*="View Invoice"]');
14+
this.emptyState = page.locator('text=/No invoices found/i');
15+
this.payMultipleButton = page.locator('button:has-text("Pay Multiple")');
16+
}
17+
18+
async goto() {
19+
await this.page.goto('/dashboard');
20+
}
21+
}

e2e/pages/InvoiceDetailPage.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
export class InvoiceDetailPage {
4+
readonly page: Page;
5+
readonly heading: Locator;
6+
readonly statusBadge: Locator;
7+
readonly payButton: Locator;
8+
readonly fundingProgress: Locator;
9+
readonly recipientList: Locator;
10+
readonly paymentList: Locator;
11+
12+
constructor(page: Page) {
13+
this.page = page;
14+
this.heading = page.getByRole('heading', { name: /Invoice #/ });
15+
this.statusBadge = page.locator('[aria-label*="Status:"]');
16+
this.payButton = page.locator('button:has-text("Pay")').first();
17+
this.fundingProgress = page.locator('[role="progressbar"]');
18+
this.recipientList = page.locator('ul[aria-label="Recipient list"]');
19+
this.paymentList = page.locator('ul[aria-label="Payment list"]');
20+
}
21+
22+
async goto(id: string) {
23+
await this.page.goto(`/invoice/${id}`);
24+
}
25+
26+
async clickPay() {
27+
await this.payButton.click();
28+
}
29+
}

e2e/pages/LandingPage.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
export class LandingPage {
4+
readonly page: Page;
5+
readonly createInvoiceCta: Locator;
6+
7+
constructor(page: Page) {
8+
this.page = page;
9+
this.createInvoiceCta = page.locator('a[href="/invoice/new"]');
10+
}
11+
12+
async goto() {
13+
await this.page.goto('/');
14+
}
15+
16+
async clickCreateInvoice() {
17+
await this.createInvoiceCta.click();
18+
}
19+
}

e2e/pages/VerifyPage.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
export class VerifyPage {
4+
readonly page: Page;
5+
readonly verifiedBadge: Locator;
6+
readonly fundedBadge: Locator;
7+
readonly statusText: Locator;
8+
readonly creatorAddress: Locator;
9+
readonly recipientItems: Locator;
10+
readonly paymentItems: Locator;
11+
readonly progressSection: Locator;
12+
readonly payButton: Locator;
13+
14+
constructor(page: Page) {
15+
this.page = page;
16+
this.verifiedBadge = page.locator('text=Verified on-chain');
17+
this.fundedBadge = page.locator('text=% funded');
18+
this.statusText = page.locator('[aria-label*="Status:"]');
19+
this.creatorAddress = page.locator('section[aria-labelledby="verify-creator-heading"]');
20+
this.recipientItems = page.locator('section[aria-labelledby="verify-recipients-heading"] li');
21+
this.paymentItems = page.locator('section[aria-labelledby="verify-payments-heading"] li');
22+
this.progressSection = page.locator('section[aria-labelledby="verify-progress-heading"]');
23+
this.payButton = page.locator('button:has-text("Pay")');
24+
}
25+
26+
async goto(id: string) {
27+
await this.page.goto(`/verify/${id}`);
28+
}
29+
}

0 commit comments

Comments
 (0)