Skip to content

Commit fd25e35

Browse files
authored
Merge pull request #339 from Calebux/feat/e2e-ticket-purchase
test(client): E2E tests for ticket purchase flow (closes #183)
2 parents fccb715 + ea822ab commit fd25e35

5 files changed

Lines changed: 232 additions & 17 deletions

File tree

client/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
"test:unit": "vitest --run",
1212
"test:e2e": "playwright test",
1313
"preview": "vite preview",
14-
"validate-env": "node scripts/validate-env.js"
14+
"validate-env": "node scripts/validate-env.js",
15+
"test:e2e": "playwright test",
16+
"test:e2e:ui": "playwright test --ui",
17+
"test:e2e:headed": "playwright test --headed"
1518
},
1619
"dependencies": {
1720
"@creit.tech/stellar-wallets-kit": "^1.7.3",
@@ -28,6 +31,7 @@
2831
"tailwindcss": "^4.1.13"
2932
},
3033
"devDependencies": {
34+
"@playwright/test": "^1.50.0",
3135
"@eslint/js": "^9.33.0",
3236
"@playwright/test": "^1.43.0",
3337
"@testing-library/jest-dom": "^5.16.5",

client/src/components/cards/RaffleCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const RaffleCard: React.FC<RaffleCardProps> = ({
102102
<p className="text-gray-600 dark:text-[#9CA3AF] text-[12px]">
103103
Entries
104104
</p>
105-
<p>{entries}</p>
105+
<p data-testid="entries-count">{entries}</p>
106106
</div>
107107
</div>
108108

@@ -166,7 +166,7 @@ const RaffleCard: React.FC<RaffleCardProps> = ({
166166
<p className="text-gray-600 dark:text-[#9CA3AF] text-[12px]">
167167
Entries
168168
</p>
169-
<p>{entries}</p>
169+
<p data-testid="entries-count">{entries}</p>
170170
</div>
171171
</div>
172172

client/src/components/landing/TrendingRaffles.tsx

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ const RaffleCardWrapper: React.FC<{
3939
onEnter: () => void;
4040
}> = ({ raffleId, onEnter }) => {
4141
const { raffle, error, isLoading } = useRaffle(raffleId);
42+
const [showSuccess, setShowSuccess] = useState(false);
43+
const [extraEntries, setExtraEntries] = useState(0);
44+
45+
const handlePurchaseSuccess = () => {
46+
setExtraEntries((n) => n + 1);
47+
setShowSuccess(true);
48+
};
4249

4350
if (isLoading) {
4451
return <RaffleCardSkeleton />;
@@ -57,19 +64,31 @@ const RaffleCardWrapper: React.FC<{
5764
}
5865

5966
return (
60-
<RaffleCard
61-
image={raffle.image}
62-
title={raffle.description}
63-
prizeValue={raffle.prizeValue}
64-
prizeCurrency={raffle.prizeCurrency}
65-
countdown={raffle.countdown}
66-
ticketPrice={raffle.ticketPriceFormatted}
67-
entries={raffle.entries}
68-
progress={raffle.progress}
69-
buttonText={raffle.buttonText}
70-
raffleId={raffle.id}
71-
onEnter={onEnter}
72-
/>
67+
<>
68+
<RaffleCard
69+
image={raffle.image}
70+
title={raffle.description}
71+
prizeValue={raffle.prizeValue}
72+
prizeCurrency={raffle.prizeCurrency}
73+
countdown={raffle.countdown}
74+
ticketPrice={raffle.ticketPriceFormatted}
75+
entries={raffle.entries + extraEntries}
76+
progress={raffle.progress}
77+
buttonText={raffle.buttonText}
78+
raffleId={raffle.id}
79+
onEnter={handlePurchaseSuccess}
80+
/>
81+
<Modal open={showSuccess} onClose={() => setShowSuccess(false)}>
82+
<SuccessfulTicket
83+
raffleName={raffle.description}
84+
onClose={() => setShowSuccess(false)}
85+
onContinue={() => {
86+
setShowSuccess(false);
87+
onEnter();
88+
}}
89+
/>
90+
</Modal>
91+
</>
7392
);
7493
};
7594

client/src/components/modals/SuccessfulTicket.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const SuccessfulTicket = ({
1616
if (!isVisible) return null;
1717

1818
return (
19-
<div className="w-full max-w-[500px] mx-auto px-4 sm:px-6">
19+
<div data-testid="success-modal" className="w-full max-w-[500px] mx-auto px-4 sm:px-6">
2020
{/* Close Button */}
2121
{onClose && (
2222
<div className="flex justify-end mb-4 sm:mb-6">
@@ -52,6 +52,7 @@ const SuccessfulTicket = ({
5252
{/* Continue Button */}
5353
<div className="w-full">
5454
<button
55+
data-testid="success-continue-btn"
5556
onClick={onContinue}
5657
className="w-full bg-[#fe3796] hover:bg-[#fe3796]/90 text-gray-900 dark:text-white font-semibold py-3 sm:py-4 px-4 sm:px-6 rounded-xl transition-colors text-sm sm:text-base"
5758
>
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { test, expect, Page } from "@playwright/test";
2+
3+
// ── Helpers ────────────────────────────────────────────────────────────────────
4+
5+
/** Navigate to /home and wait for at least one Enter Raffle button to be visible. */
6+
async function gotoHome(page: Page) {
7+
await page.goto("/home");
8+
await expect(
9+
page.getByTestId("enter-raffle-btn").first()
10+
).toBeVisible({ timeout: 10_000 });
11+
}
12+
13+
/** Click the first Enter Raffle button and return a reference to it. */
14+
async function clickFirstEnterButton(page: Page) {
15+
const btn = page.getByTestId("enter-raffle-btn").first();
16+
await btn.click();
17+
return btn;
18+
}
19+
20+
/** Wait for the success modal to appear (allows for the 900 ms demo delay). */
21+
async function waitForSuccessModal(page: Page) {
22+
const modal = page.getByTestId("success-modal");
23+
await expect(modal).toBeVisible({ timeout: 5_000 });
24+
return modal;
25+
}
26+
27+
// ── Tests ──────────────────────────────────────────────────────────────────────
28+
29+
test.describe("Ticket purchase flow", () => {
30+
test.describe("page load", () => {
31+
test("navigates to /home and shows raffle cards with Enter Raffle buttons", async ({
32+
page,
33+
}) => {
34+
await gotoHome(page);
35+
const buttons = page.getByTestId("enter-raffle-btn");
36+
await expect(buttons.first()).toBeVisible();
37+
await expect(buttons.first()).toHaveText("Enter Raffle");
38+
});
39+
40+
test("raffle cards display an entries count", async ({ page }) => {
41+
await gotoHome(page);
42+
const count = page.getByTestId("entries-count").first();
43+
await expect(count).toBeVisible();
44+
// Should be a non-empty number
45+
const text = await count.innerText();
46+
expect(Number(text)).toBeGreaterThanOrEqual(0);
47+
});
48+
});
49+
50+
test.describe("button loading state", () => {
51+
test('shows "Processing..." and disables the button while purchase is in-flight', async ({
52+
page,
53+
}) => {
54+
await gotoHome(page);
55+
const btn = await clickFirstEnterButton(page);
56+
await expect(btn).toHaveText("Processing...");
57+
await expect(btn).toBeDisabled();
58+
});
59+
60+
test("re-enables the button after the purchase completes", async ({
61+
page,
62+
}) => {
63+
await gotoHome(page);
64+
const btn = page.getByTestId("enter-raffle-btn").first();
65+
await btn.click();
66+
// Wait for the success modal — means the purchase finished
67+
await waitForSuccessModal(page);
68+
// After modal is shown the button should be enabled again
69+
await expect(btn).toBeEnabled();
70+
});
71+
});
72+
73+
test.describe("success modal", () => {
74+
test('success modal appears with "Let\'s go!!!" heading', async ({
75+
page,
76+
}) => {
77+
await gotoHome(page);
78+
await clickFirstEnterButton(page);
79+
const modal = await waitForSuccessModal(page);
80+
await expect(modal).toContainText("Let's go!!!");
81+
});
82+
83+
test("success modal contains the raffle name", async ({ page }) => {
84+
await gotoHome(page);
85+
await clickFirstEnterButton(page);
86+
const modal = await waitForSuccessModal(page);
87+
// The modal says "Your tickets purchase for <raffleName> was successful."
88+
await expect(modal).toContainText("successful");
89+
});
90+
91+
test("success modal has a Continue button", async ({ page }) => {
92+
await gotoHome(page);
93+
await clickFirstEnterButton(page);
94+
await waitForSuccessModal(page);
95+
await expect(page.getByTestId("success-continue-btn")).toBeVisible();
96+
});
97+
98+
test("Close (×) button dismisses the success modal", async ({
99+
page,
100+
}) => {
101+
await gotoHome(page);
102+
await clickFirstEnterButton(page);
103+
await waitForSuccessModal(page);
104+
await page.getByLabel("Close modal").click();
105+
await expect(page.getByTestId("success-modal")).not.toBeVisible();
106+
});
107+
108+
test("Continue button dismisses the success modal", async ({ page }) => {
109+
await gotoHome(page);
110+
await clickFirstEnterButton(page);
111+
await waitForSuccessModal(page);
112+
await page.getByTestId("success-continue-btn").click();
113+
await expect(page.getByTestId("success-modal")).not.toBeVisible();
114+
});
115+
116+
test("ESC key closes the success modal", async ({ page }) => {
117+
await gotoHome(page);
118+
await clickFirstEnterButton(page);
119+
await waitForSuccessModal(page);
120+
await page.keyboard.press("Escape");
121+
await expect(page.getByTestId("success-modal")).not.toBeVisible();
122+
});
123+
124+
test("clicking the backdrop closes the success modal", async ({
125+
page,
126+
}) => {
127+
await gotoHome(page);
128+
await clickFirstEnterButton(page);
129+
await waitForSuccessModal(page);
130+
// Click the semi-transparent backdrop (outside the modal dialog)
131+
await page.mouse.click(10, 10);
132+
await expect(page.getByTestId("success-modal")).not.toBeVisible();
133+
});
134+
});
135+
136+
test.describe("ticket count update", () => {
137+
test("entries count increments by 1 after a successful purchase", async ({
138+
page,
139+
}) => {
140+
await gotoHome(page);
141+
const entriesEl = page.getByTestId("entries-count").first();
142+
const before = Number(await entriesEl.innerText());
143+
144+
await clickFirstEnterButton(page);
145+
await waitForSuccessModal(page);
146+
147+
await expect(entriesEl).toHaveText(String(before + 1));
148+
});
149+
150+
test("entries count increments again on a second purchase", async ({
151+
page,
152+
}) => {
153+
await gotoHome(page);
154+
const entriesEl = page.getByTestId("entries-count").first();
155+
const before = Number(await entriesEl.innerText());
156+
157+
// First purchase
158+
await clickFirstEnterButton(page);
159+
await waitForSuccessModal(page);
160+
await page.getByTestId("success-continue-btn").click();
161+
await expect(page.getByTestId("success-modal")).not.toBeVisible();
162+
163+
// Second purchase on the same card
164+
await clickFirstEnterButton(page);
165+
await waitForSuccessModal(page);
166+
167+
await expect(entriesEl).toHaveText(String(before + 2));
168+
});
169+
});
170+
171+
test.describe("navigation", () => {
172+
test("clicking the raffle card link navigates to the detail page", async ({
173+
page,
174+
}) => {
175+
await gotoHome(page);
176+
// The <Link> wraps the card image/content
177+
await page.getByRole("link").first().click();
178+
await expect(page).toHaveURL(/\/details\?raffle=\d+/);
179+
});
180+
181+
test("Continue button on success modal navigates to the raffle detail page", async ({
182+
page,
183+
}) => {
184+
await gotoHome(page);
185+
await clickFirstEnterButton(page);
186+
await waitForSuccessModal(page);
187+
await page.getByTestId("success-continue-btn").click();
188+
await expect(page).toHaveURL(/\/details\?raffle=\d+/);
189+
});
190+
});
191+
});

0 commit comments

Comments
 (0)