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
67 changes: 67 additions & 0 deletions e2e/group-creation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { test, expect } from "@playwright/test";

test.describe("Group Creation Form", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/groups/new");
});

test("renders the create group form", async ({ page }) => {
// Check that the form page loads
await expect(page.locator("form, [data-testid='create-group-form']").first()).toBeVisible().catch(() => {
// If no explicit form, check for relevant inputs
return expect(page.locator("input, textarea, select").first()).toBeVisible();
});
});

test("shows validation error when submitting empty form", async ({
page,
}) => {
// Find and click the submit button
const submitBtn = page.getByRole("button", {
name: /create|submit|save/i,
});
if (await submitBtn.isVisible()) {
await submitBtn.click();
// Check for any validation feedback (HTML5 or custom)
const hasError =
(await page.locator("[class*='error'], [class*='invalid'], :invalid").count()) > 0;
expect(hasError).toBeTruthy();
}
});

test("group name field is present and accepts input", async ({ page }) => {
const nameInput = page
.getByLabel(/group name/i)
.or(page.getByPlaceholder(/group name/i))
.or(page.locator('input[name="name"]'));

if (await nameInput.isVisible()) {
await nameInput.fill("Test Savings Group");
await expect(nameInput).toHaveValue("Test Savings Group");
}
});

test("contribution amount field accepts numeric input", async ({ page }) => {
const amountInput = page
.getByLabel(/contribution/i)
.or(page.getByPlaceholder(/amount/i))
.or(page.locator('input[name="contributionAmount"], input[type="number"]').first());

if (await amountInput.isVisible()) {
await amountInput.fill("100");
await expect(amountInput).toHaveValue("100");
}
});

test("max members field accepts numeric input", async ({ page }) => {
const membersInput = page
.getByLabel(/max members/i)
.or(page.getByPlaceholder(/members/i))
.or(page.locator('input[name="maxMembers"]'));

if (await membersInput.isVisible()) {
await membersInput.fill("5");
await expect(membersInput).toHaveValue("5");
}
});
});
55 changes: 55 additions & 0 deletions e2e/home.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { test, expect } from "@playwright/test";

test.describe("Landing Page", () => {
test("renders the hero section with correct heading", async ({ page }) => {
await page.goto("/");
await expect(
page.getByRole("heading", {
name: "Decentralized Group Savings for Everyone",
})
).toBeVisible();
});

test("displays the SoroSave brand name in the navbar", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("link", { name: "SoroSave" })).toBeVisible();
});

test("shows Browse Groups and Create a Group CTA buttons", async ({
page,
}) => {
await page.goto("/");
await expect(
page.getByRole("link", { name: "Browse Groups" })
).toBeVisible();
await expect(
page.getByRole("link", { name: "Create a Group" })
).toBeVisible();
});

test("displays the How It Works section with 4 steps", async ({ page }) => {
await page.goto("/");
await expect(
page.getByRole("heading", { name: "How It Works" })
).toBeVisible();
await expect(page.getByText("Create a Group")).toBeVisible();
await expect(page.getByText("Members Join")).toBeVisible();
await expect(page.getByText("Contribute Each Cycle")).toBeVisible();
await expect(page.getByText("Receive the Pot")).toBeVisible();
});

test("displays the Why SoroSave section", async ({ page }) => {
await page.goto("/");
await expect(
page.getByRole("heading", { name: "Why SoroSave?" })
).toBeVisible();
await expect(page.getByText("Trustless")).toBeVisible();
await expect(page.getByText("Transparent")).toBeVisible();
await expect(page.getByText("Low Cost")).toBeVisible();
});

test("footer has GitHub link", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("link", { name: "GitHub" })).toBeVisible();
});
});
38 changes: 38 additions & 0 deletions e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect } from "@playwright/test";

test.describe("Navigation", () => {
test("navigates to groups page via navbar link", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Groups" }).first().click();
await expect(page).toHaveURL(/.*\/groups/);
});

test("navigates to create group page via navbar link", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Create Group" }).first().click();
await expect(page).toHaveURL(/.*\/groups\/new/);
});

test("navigates to groups page from hero CTA", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Browse Groups" }).click();
await expect(page).toHaveURL(/.*\/groups/);
});

test("navigates to create group from hero CTA", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Create a Group" }).click();
await expect(page).toHaveURL(/.*\/groups\/new/);
});

test("SoroSave logo navigates back to home", async ({ page }) => {
await page.goto("/groups");
await page.getByRole("link", { name: "SoroSave" }).click();
await expect(page).toHaveURL(/.*\/$/);
});

test("page title is correct on home page", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/SoroSave/);
});
});
67 changes: 67 additions & 0 deletions e2e/wallet.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { test, expect } from "@playwright/test";

/**
* Wallet connection tests with mocked Freighter extension.
* These tests mock the @stellar/freighter-api to simulate wallet behavior
* without requiring the actual browser extension.
*/
test.describe("Wallet Connection", () => {
test.beforeEach(async ({ page }) => {
// Mock the Freighter wallet API
await page.addInitScript(() => {
// Mock window.freighter to simulate installed extension
(window as any).freighter = {
isConnected: () => Promise.resolve(true),
getPublicKey: () =>
Promise.resolve("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGKZM7CPKKRJOCUAVHKIO1"),
signTransaction: (xdr: string) => Promise.resolve(xdr),
getNetwork: () => Promise.resolve("TESTNET"),
};

// Mock the freighter-api module responses
Object.defineProperty(window, "freighterMock", {
value: {
installed: true,
publicKey: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGKZM7CPKKRJOCUAVHKIO1",
},
});
});
});

test("Connect Wallet button is visible in navbar", async ({ page }) => {
await page.goto("/");
const connectBtn = page.getByRole("button", {
name: /connect wallet|connect|freighter/i,
});
await expect(connectBtn).toBeVisible();
});

test("wallet connect button shows correct initial state", async ({
page,
}) => {
await page.goto("/");
// Before connection, should show connect text
const connectBtn = page
.getByRole("button", { name: /connect/i })
.first();
if (await connectBtn.isVisible()) {
const text = await connectBtn.textContent();
expect(text?.toLowerCase()).toContain("connect");
}
});

test("clicking connect wallet triggers connection flow", async ({ page }) => {
await page.goto("/");
const connectBtn = page.getByRole("button", {
name: /connect wallet/i,
});
if (await connectBtn.isVisible()) {
await connectBtn.click();
// After clicking, either shows address or an error about missing extension
await page.waitForTimeout(500);
// The button should have changed or an error should be shown
const pageText = await page.textContent("body");
expect(pageText).toBeTruthy();
}
});
});
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "playwright show-report"
},
"dependencies": {
"@sorosave/sdk": "workspace:*",
Expand All @@ -16,6 +19,7 @@
"react-dom": "^18.3.0"
},
"devDependencies": {
"@playwright/test": "^1.44.0",
"@types/node": "^20.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
Expand Down
36 changes: 36 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { defineConfig, devices } from "@playwright/test";

/**
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "Mobile Chrome",
use: { ...devices["Pixel 5"] },
},
],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});