Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,41 @@ jobs:
- name: Check Formatting
run: npx prettier --check .

verify-e2e-tests:
name: Run E2E Tests (Playwright)
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Install Dependencies
run: pnpm install --frozen-lockfile

- name: Install Playwright Browsers
run: pnpm --filter web exec playwright install --with-deps chromium

- name: Build Packages (SDK & Web App)
run: pnpm build
env:
NEXT_PUBLIC_REGISTRY_CONTRACT_ID: CABGWVIZFF62FG67ZGFEP67NEEY4WYTMFURDMFTKKNRDAFPKPOJDTN4C
NEXT_PUBLIC_INVOICE_CONTRACT_ID: CA4O3MR7LWHRSUDBNU6FY6UDFFYBN7TGBZXBDZB4OYYXFYXIFJ6RJF6B
NEXT_PUBLIC_POOL_CONTRACT_ID: CAKEWH7SJCXGV2MH2WZYIX3QDPTSSBQFXYVYBOWAGLNBBZMPLE2US6CS
NEXT_PUBLIC_ESCROW_CONTRACT_ID: CAJWGUKDTTC3SKN4RAAY72J4DVIIYSCFHX6GIMNTT22ABMISJK4GBCEH

- name: Run E2E Tests
run: pnpm test:e2e

verify-go-indexer:
name: Verify Go Indexer & API
runs-on: ubuntu-latest
Expand All @@ -57,7 +92,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.22"
go-version-file: "indexer/go.mod"

- name: Get Dependencies
run: |
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,10 @@ pnpm-debug.log*
.DS_Store
Thumbs.db
.neon
create_pr.ps1
build_output.txt

# Playwright
playwright-report/
test-results/
blob-report/
5 changes: 4 additions & 1 deletion apps/web/e2e/fixtures/freighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export const test = base.extend({
isConnected: () => Promise.resolve(true),
isAllowed: () => Promise.resolve(true),
setAllowed: () => Promise.resolve(),
requestAccess: () => Promise.resolve(""),
requestAccess: () =>
Promise.resolve(
"GBMOCKWALLETADDRESSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
),
signTransaction: (xdr: string) => Promise.resolve("signed-xdr-mock"),
signAuthEntry: () => Promise.resolve("signed-auth-mock"),
getPublicKey: () =>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/e2e/invoice-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ test.describe("Invoice Lifecycle - Happy Path", () => {
await page.getByRole("button", { name: /Create Invoice/i }).click();

await page
.getByLabelText(/Buyer Address/i)
.getByLabel(/Buyer Address/i)
.fill("GBBUYERXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
await page.getByLabelText(/Face Value/i).fill("1000");
await page.getByLabel(/Face Value/i).fill("1000");
await page.getByRole("button", { name: /Next/i }).click();

await page.getByRole("button", { name: /Confirm & Create/i }).click();
Expand Down
224 changes: 224 additions & 0 deletions apps/web/e2e/wallet-connect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { expect, test as baseTest } from "@playwright/test";
import { test } from "./fixtures/freighter";

test.describe("Wallet Connect & SEP-10 Auth Flow", () => {
const MOCK_ADDRESS = "GBMOCKWALLETADDRESSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const MOCK_CHALLENGE_XDR = "AAAAAQAAAACmock_challenge_xdr_transaction";
const MOCK_JWT_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.mock_token";

test.beforeEach(async ({ page }) => {
// Intercept indexer SEP-10 authentication API calls
await page.route("**/auth?address=*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
transaction: MOCK_CHALLENGE_XDR,
network_passphrase: "Test SDF Network ; November 2015",
}),
});
});

await page.route("**/auth", async (route) => {
if (route.request().method() === "POST") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
token: MOCK_JWT_TOKEN,
}),
});
} else {
await route.continue();
}
});

// Mock other indexer calls if needed
await page.route("**/pool/stats", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
total_deposits: "10000000000",
total_funded: "5000000000",
available_liquidity: "5000000000",
utilization_rate_bps: 5000,
total_yield_distributed: "100000000",
active_invoice_count: 5,
total_shares: "10000000000",
}),
});
});

await page.route("**/stats", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
total_usdc_financed: "500000.00",
active_invoice_count: 5,
total_invoices: 12,
total_repaid: 7,
total_defaulted: 0,
average_yield_bps: 850,
pool_utilization_bps: 5000,
registered_issuers: 8,
}),
});
});
});

test("1. Connect wallet successfully with injected Freighter mock", async ({
page,
}) => {
await page.goto("/");

// Verify initial disconnected state
const connectBtn = page.getByRole("button", { name: /CONNECT WALLET/i });
await expect(connectBtn).toBeVisible();

// Click connect button
await connectBtn.click();

// Verify connected address (formatted address: GBMOCK...AAAA or GBMOCK...XXXX)
await expect(page.getByText(/GBMOCK\.\.\.XXXX/i)).toBeVisible();

// Verify network indicator
await expect(page.getByText("Testnet")).toBeVisible();
});

test("2. Execute SEP-10 challenge-sign-verify authentication flow", async ({
page,
}) => {
await page.goto("/");

// Connect wallet first
const connectBtn = page.getByRole("button", { name: /CONNECT WALLET/i });
await connectBtn.click();
await expect(page.getByText(/GBMOCK\.\.\.XXXX/i)).toBeVisible();

// Execute SEP-10 login flow programmatically via window evaluate or store
const authResult = await page.evaluate(async () => {
const walletStore = (window as any).__WALLET_STORE__ || {};
const { fetchChallenge, verifyChallenge } = await import("@/lib/api");
const { signTransaction } = await import("@stellar/freighter-api");

const address = "GBMOCKWALLETADDRESSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const challenge = await fetchChallenge(address);
const signedXdr = await signTransaction(challenge.transaction, {
network: "TESTNET",
networkPassphrase: challenge.network_passphrase,
accountToSign: address,
});
const { token } = await verifyChallenge(signedXdr);

return {
challengeTx: challenge.transaction,
signedXdr,
token,
};
});

expect(authResult.challengeTx).toBe(MOCK_CHALLENGE_XDR);
expect(authResult.signedXdr).toBe("signed-xdr-mock");
expect(authResult.token).toBe(MOCK_JWT_TOKEN);
});

test("3. Disconnect wallet and reset state", async ({ page }) => {
await page.goto("/");

// Connect wallet
const connectBtn = page.getByRole("button", { name: /CONNECT WALLET/i });
await connectBtn.click();
await expect(page.getByText(/GBMOCK\.\.\.XXXX/i)).toBeVisible();

// Disconnect wallet
const disconnectBtn = page.getByRole("button", {
name: /Disconnect wallet/i,
});
await expect(disconnectBtn).toBeVisible();
await disconnectBtn.click();

// Verify UI resets to disconnected state
await expect(
page.getByRole("button", { name: /CONNECT WALLET/i }),
).toBeVisible();
await expect(page.getByText(/GBMOCK\.\.\.XXXX/i)).not.toBeVisible();

// Verify Zustand storage in localStorage is cleared/reset
const storageState = await page.evaluate(() => {
const item = localStorage.getItem("wallet-storage");
return item ? JSON.parse(item) : null;
});

expect(storageState?.state?.address).toBeNull();
});

test("4. Reconnection and state persistence across reloads", async ({
page,
}) => {
await page.goto("/");

// Connect wallet
await page.getByRole("button", { name: /CONNECT WALLET/i }).click();
await expect(page.getByText(/GBMOCK\.\.\.XXXX/i)).toBeVisible();

// Reload page
await page.reload();

// Verify state persists after reload
await expect(page.getByText(/GBMOCK\.\.\.XXXX/i)).toBeVisible();

// Disconnect and reconnect
await page.getByRole("button", { name: /Disconnect wallet/i }).click();
await expect(
page.getByRole("button", { name: /CONNECT WALLET/i }),
).toBeVisible();

await page.getByRole("button", { name: /CONNECT WALLET/i }).click();
await expect(page.getByText(/GBMOCK\.\.\.XXXX/i)).toBeVisible();
});
});

baseTest.describe("Wallet Error Handling & Uninstalled Extension", () => {
baseTest(
"Displays user cancellation message when request is rejected",
async ({ page }) => {
await page.addInitScript(() => {
window.freighter = {
isConnected: () => Promise.resolve(true),
isAllowed: () => Promise.resolve(true),
setAllowed: () => Promise.resolve(),
requestAccess: () =>
Promise.reject(new Error("User rejected this request")),
signTransaction: () => Promise.resolve(""),
signAuthEntry: () => Promise.resolve(""),
getPublicKey: () => Promise.resolve(""),
getNetworkDetails: () => Promise.resolve({ network: "TESTNET" }),
};
});

await page.goto("/");
const connectBtn = page.getByRole("button", { name: /CONNECT WALLET/i });
await connectBtn.click();

await expect(
page.getByText(/You cancelled the connection request/i),
).toBeVisible();
},
);

baseTest(
"Displays Install Freighter link when extension is missing",
async ({ page }) => {
await page.addInitScript(() => {
delete (window as any).freighter;
});

await page.goto("/");
await expect(
page.getByRole("link", { name: /Install Freighter/i }),
).toBeVisible();
},
);
});
5 changes: 4 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"start": "next start",
"lint": "next lint",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage --passWithNoTests"
"test:coverage": "vitest run --coverage --passWithNoTests",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.5",
Expand All @@ -34,6 +36,7 @@
"zustand": "^5.0.14"
},
"devDependencies": {
"@playwright/test": "^1.49.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^20",
Expand Down
4 changes: 4 additions & 0 deletions apps/web/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs"
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "pnpm --filter @trusttrove/sdk build && pnpm --filter web build",
"lint": "pnpm --filter web lint",
"test": "pnpm --filter @trusttrove/sdk test && pnpm --filter web test",
"test:e2e": "pnpm --filter web test:e2e",
"typecheck": "pnpm -r exec tsc --noEmit"
},
"devDependencies": {
Expand Down
Loading
Loading