From ccf01833137def109282074244c55fb39d0daebe Mon Sep 17 00:00:00 2001 From: sandrawillow001-afk Date: Sat, 20 Jun 2026 21:42:10 +0000 Subject: [PATCH 1/2] feat: add virtual wallet injection & webServer config for E2E tests (#16) - Add webServer auto-start config to playwright.config.ts - Add virtual wallet injection helpers (injectWalletSession) - Add 7 new E2E tests: 5 virtual injection + 2 edge cases - Add test-results and playwright-report to eslint ignores - Add playwright output dirs to .gitignore --- .gitignore | 4 + eslint.config.mjs | 2 +- package-lock.json | 7 +- playwright.config.ts | 6 + tests/e2e/wallet.spec.ts | 236 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 242 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 5ef6a52..1578133 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# playwright +/test-results/ +/playwright-report/ diff --git a/eslint.config.mjs b/eslint.config.mjs index 02e4549..8474a18 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,7 @@ import tsEslint from "typescript-eslint"; export default [ { - ignores: [".next/**", "node_modules/**", "out/**", "dist/**"], + ignores: [".next/**", "node_modules/**", "out/**", "dist/**", "test-results/**", "playwright-report/**"], }, ...tsEslint.configs.recommended, { diff --git a/package-lock.json b/package-lock.json index 7dbc63a..dc1c46e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4939,7 +4939,6 @@ "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, "license": "MIT", "optional": true, @@ -6482,9 +6481,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.13", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz", - "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz", + "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==", "funding": [ { "type": "github", diff --git a/playwright.config.ts b/playwright.config.ts index 0d2b97a..0417120 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,4 +10,10 @@ export default defineConfig({ baseURL: "http://localhost:3000", trace: "on-first-retry", }, + webServer: { + command: "npm run dev", + url: "http://localhost:3000", + reuseExistingServer: !!process.env.CI, + timeout: 120_000, + }, }); diff --git a/tests/e2e/wallet.spec.ts b/tests/e2e/wallet.spec.ts index 3c3cb8c..c5fadd4 100644 --- a/tests/e2e/wallet.spec.ts +++ b/tests/e2e/wallet.spec.ts @@ -1,4 +1,76 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, type Page } from "@playwright/test"; + +// --------------------------------------------------------------------------- +// Virtual Wallet Injection Helpers +// --------------------------------------------------------------------------- + +interface InjectedWalletSession { + address: string; + network: string; + signature: string; + expiresAt: number; +} + +/** Generate a deterministic Stellar-compatible address for testing. */ +function generateTestAddress(seed: number): string { + // G-prefixed base32-like public key matching Stellar address format + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + // Stellar public keys are G + 55 chars = 56 total + const body = Array.from({ length: 55 }, (_, i) => + chars.charAt((seed * 7 + i * 13) % chars.length) + ).join(""); + return `G${body}`; +} + +/** Create a fake auth session payload suitable for localStorage injection. */ +function createTestSession(address: string): InjectedWalletSession { + return { + address, + network: "testnet", + signature: "00".repeat(64), + expiresAt: Date.now() + 30 * 60 * 1000, // 30 minutes + }; +} + +/** Inject a connected wallet session into the browser via localStorage. + * Navigates to the app first so localStorage is accessible, injects session, + * then reloads so the app picks up the injected state. */ +async function injectWalletSession( + page: Page, + address?: string +): Promise { + const addr = address ?? generateTestAddress(Date.now()); + const session = createTestSession(addr); + + // Must be on the app's origin before touching localStorage + await page.goto("/"); + + await page.evaluate( + ({ session: s }) => { + localStorage.setItem("utility-auth-session", JSON.stringify(s)); + localStorage.setItem( + "utility-auth-secret", + // 56-char Stellar secret key (S-prefixed) + "S" + "A".repeat(55) + ); + }, + { session } + ); + + // Reload so the app reads the newly injected session + await page.reload(); + + return addr; +} + +/** Format a Stellar address for display (first 6 … last 4). */ +function truncateAddress(address: string): string { + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} + +// --------------------------------------------------------------------------- +// Wallet Connection Flow (real Stellar SDK via UI) +// --------------------------------------------------------------------------- test.describe("Wallet Connection Flow", () => { test.beforeEach(async ({ page }) => { @@ -12,30 +84,178 @@ test.describe("Wallet Connection Flow", () => { await expect(connectBtn).toBeVisible(); }); - test("should connect wallet and show address", async ({ page }) => { + test("should connect wallet and show truncated address", async ({ + page, + }) => { const connectBtn = page.getByRole("button", { name: /connect wallet/i }); await connectBtn.click(); - await expect(page.getByText(/^G[A-Z0-9]{5}\.{3}[A-Z0-9]{4}$/)).toBeVisible(); + await expect( + page.getByText(/^G[A-Z0-9]{5}\.{3}[A-Z0-9]{4}$/) + ).toBeVisible(); }); - test("should disconnect wallet and show connect button", async ({ page }) => { + test("should disconnect wallet and show connect button", async ({ + page, + }) => { await page.getByRole("button", { name: /connect wallet/i }).click(); - await expect(page.getByText(/^G[A-Z0-9]{5}\.{3}[A-Z0-9]{4}$/)).toBeVisible(); + await expect( + page.getByText(/^G[A-Z0-9]{5}\.{3}[A-Z0-9]{4}$/) + ).toBeVisible(); await page.getByRole("button", { name: /disconnect/i }).click(); - await expect(page.getByRole("button", { name: /connect wallet/i })).toBeVisible(); + await expect( + page.getByRole("button", { name: /connect wallet/i }) + ).toBeVisible(); }); - test("should persist wallet session across page reload", async ({ page }) => { + test("should persist wallet session across page reload", async ({ + page, + }) => { await page.getByRole("button", { name: /connect wallet/i }).click(); - const address = await page.getByText(/^G[A-Z0-9]{5}\.{3}[A-Z0-9]{4}$/).textContent(); + const address = await page + .getByText(/^G[A-Z0-9]{5}\.{3}[A-Z0-9]{4}$/) + .textContent(); await page.reload(); await expect(page.getByText(address!)).toBeVisible(); }); }); +// --------------------------------------------------------------------------- +// Virtual Wallet Injection Tests +// --------------------------------------------------------------------------- + +test.describe("Virtual Wallet Injection", () => { + test("should show connected state when session is injected", async ({ + page, + }) => { + const address = await injectWalletSession(page); + + // Address should be visible without clicking Connect + await expect( + page.getByText(truncateAddress(address)) + ).toBeVisible(); + + // Disconnect button should be visible + await expect( + page.getByRole("button", { name: /disconnect/i }) + ).toBeVisible(); + + // Connect button should NOT be visible + await expect( + page.getByRole("button", { name: /connect wallet/i }) + ).not.toBeVisible(); + }); + + test("should show multiple test addresses correctly", async ({ page }) => { + const address = generateTestAddress(42); + await injectWalletSession(page, address); + + // Verify the exact injected address is displayed + await expect( + page.getByText(truncateAddress(address)) + ).toBeVisible(); + }); + + test("should show connect button after clearing injected session", async ({ + page, + }) => { + // First inject a session + await injectWalletSession(page); + + // Click disconnect to clear + await page.getByRole("button", { name: /disconnect/i }).click(); + + // Connect button should reappear + await expect( + page.getByRole("button", { name: /connect wallet/i }) + ).toBeVisible(); + }); + + test("should survive page reload with injected session", async ({ + page, + }) => { + const address = await injectWalletSession(page); + + await expect( + page.getByText(truncateAddress(address)) + ).toBeVisible(); + + // Reload should preserve the session + await page.reload(); + await expect( + page.getByText(truncateAddress(address)) + ).toBeVisible(); + }); + + test("should not show connected state with expired session", async ({ + page, + }) => { + const address = generateTestAddress(99); + const expiredSession: InjectedWalletSession = { + address, + network: "testnet", + signature: "00".repeat(64), + expiresAt: Date.now() - 1000, // expired 1 second ago + }; + + // Navigate first so localStorage is accessible, then inject expired session + await page.goto("/"); + await page.evaluate((session) => { + localStorage.setItem("utility-auth-session", JSON.stringify(session)); + localStorage.setItem("utility-auth-secret", "S" + "A".repeat(55)); + }, expiredSession); + await page.reload(); + + // Expired session should show connect button + await expect( + page.getByRole("button", { name: /connect wallet/i }) + ).toBeVisible(); + }); +}); + +// --------------------------------------------------------------------------- +// Edge cases +// --------------------------------------------------------------------------- + +test.describe("Wallet Edge Cases", () => { + test("should handle corrupt localStorage gracefully", async ({ page }) => { + await page.goto("/"); + await page.evaluate(() => { + localStorage.setItem("utility-auth-session", "not-valid-json{"); + localStorage.setItem("utility-auth-secret", "".repeat(56)); + }); + await page.reload(); + + // Should fall back to disconnected state — no crash + await expect( + page.getByRole("button", { name: /connect wallet/i }) + ).toBeVisible(); + }); + + test("should handle missing secret gracefully", async ({ page }) => { + const address = generateTestAddress(77); + const session = createTestSession(address); + + await page.goto("/"); + await page.evaluate((s) => { + localStorage.setItem("utility-auth-session", JSON.stringify(s)); + // Deliberately omit utility-auth-secret + }, session); + await page.reload(); + + // Should still show the address from the session (address is in session, not secret) + await expect( + page.getByText(truncateAddress(address)) + ).toBeVisible(); + }); +}); + +// --------------------------------------------------------------------------- +// Dashboard Integration +// --------------------------------------------------------------------------- + test.describe("Dashboard Integration", () => { test("should render the grid map canvas", async ({ page }) => { await page.goto("/"); From 41fe103b2517d02039c79b38895979256523e071 Mon Sep 17 00:00:00 2001 From: sandrawillow001-afk Date: Sat, 20 Jun 2026 21:45:55 +0000 Subject: [PATCH 2/2] fix: add .catch() to async keypair import to suppress unhandled rejection (#16) --- src/hooks/useWeb3Auth.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hooks/useWeb3Auth.ts b/src/hooks/useWeb3Auth.ts index 8629ffe..976b87c 100644 --- a/src/hooks/useWeb3Auth.ts +++ b/src/hooks/useWeb3Auth.ts @@ -36,6 +36,9 @@ export function useWeb3Auth(): UseWeb3AuthReturn { keypairRef.current = StellarKeypair.fromSecret( localStorage.getItem("utility-auth-secret") || "" ); + }).catch(() => { + // Secret is invalid or not available; keypair remains null. + // Signing will throw a descriptive error when attempted. }); } else { localStorage.removeItem("utility-auth-session");