test(e2e): add playwright smoke coverage

This commit is contained in:
2026-06-25 14:19:38 +08:00
parent 6081f2896a
commit 5879b7acf3
10 changed files with 402 additions and 8 deletions
+40
View File
@@ -0,0 +1,40 @@
# E2E tests
This project uses Playwright for end-to-end tests.
## Test tiers
- `mock`: deterministic browser tests against the local Next.js app with mocked backend APIs.
- `real-backend`: smoke tests against a deployed frontend and real backend APIs.
- `prod-smoke`: production smoke tests. Keep these non-destructive: no real OAuth completion, no real payment submission, and no high-volume chat actions.
## Commands
```bash
pnpm test:e2e
pnpm test:e2e:chrome
pnpm test:e2e:real
pnpm test:e2e:prod
```
`pnpm test:e2e` uses Playwright-managed Chromium. If the browser binary is not installed yet, run:
```bash
pnpm exec playwright install chromium
```
For local development on macOS, `pnpm test:e2e:chrome` can reuse the installed Google Chrome.
## Environment overrides
```bash
PLAYWRIGHT_BASE_URL=https://frontend-test.banlv-ai.com \
E2E_API_BASE_URL=https://proapi.banlv-ai.com \
pnpm test:e2e:real
```
```bash
PLAYWRIGHT_BASE_URL=https://cozsweet.com \
E2E_API_BASE_URL=https://proapi.banlv-ai.com \
pnpm test:e2e:prod
```
+32
View File
@@ -0,0 +1,32 @@
import type { Page } from "@playwright/test";
import {
apiEnvelope,
chatSendResponse,
emptyChatHistoryResponse,
guestLoginResponse,
paymentPlansResponse,
vipStatusResponse,
} from "./test-data";
export async function mockCoreApis(page: Page): Promise<void> {
await page.route("**/api/auth/guest", async (route) => {
await route.fulfill({ json: apiEnvelope(guestLoginResponse) });
});
await page.route("**/api/chat/history**", async (route) => {
await route.fulfill({ json: apiEnvelope(emptyChatHistoryResponse) });
});
await page.route("**/api/chat/send", async (route) => {
await route.fulfill({ json: apiEnvelope(chatSendResponse) });
});
await page.route("**/api/payment/plans**", async (route) => {
await route.fulfill({ json: apiEnvelope(paymentPlansResponse) });
});
await page.route("**/api/payment/vip-status**", async (route) => {
await route.fulfill({ json: apiEnvelope(vipStatusResponse) });
});
}
+110
View File
@@ -0,0 +1,110 @@
export function apiEnvelope<T>(data: T) {
return {
code: 200,
message: "success",
success: true,
data,
};
}
export const e2eUser = {
id: "user_e2e",
username: "E2E User",
email: "e2e@example.com",
platform: "web",
intimacy: 42,
dolBalance: 0,
relationshipStage: "close_friend",
currentMood: "happy",
preferredLanguage: "en",
createdAt: "2026-06-25T00:00:00.000Z",
lastMessageAt: "",
avatarUrl: "",
loginProvider: "guest",
isGuest: true,
isVip: false,
voiceMinutesRemaining: 0,
};
export const guestLoginResponse = {
token: "e2e-guest-token",
deviceId: "e2e-device-id",
userId: e2eUser.id,
isDeviceUser: true,
expiresIn: 2_592_000,
user: e2eUser,
};
export const emptyChatHistoryResponse = {
messages: [],
total: 0,
limit: 50,
offset: 0,
isVip: false,
privateFreeLimit: 0,
privateUsedToday: 0,
privateCanViewFree: false,
};
export const chatSendResponse = {
reply: "Hello from the E2E boyfriend.",
audioUrl: "",
messageId: "msg_e2e_reply_001",
isGuest: true,
timestamp: 1_782_356_425_363,
image: {
type: null,
url: null,
},
lockDetail: {
locked: false,
showContent: true,
showUpgrade: false,
reason: null,
hint: null,
detail: null,
},
};
export const paymentPlansResponse = {
plans: [
{
plan_id: "vip_monthly",
plan_name: "Monthly",
order_type: "vip_monthly",
amount_cents: 999,
original_amount_cents: 1999,
daily_price_cents: 33,
currency: "USD",
vip_days: 30,
dol_amount: null,
},
{
plan_id: "vip_quarterly",
plan_name: "Quarterly",
order_type: "vip_quarterly",
amount_cents: 2499,
original_amount_cents: 5999,
daily_price_cents: 28,
currency: "USD",
vip_days: 90,
dol_amount: null,
},
{
plan_id: "dol_voice_30",
plan_name: "Voice Pack",
order_type: "dol_voice",
amount_cents: 499,
original_amount_cents: null,
daily_price_cents: null,
currency: "USD",
vip_days: null,
dol_amount: 30,
},
],
};
export const vipStatusResponse = {
isVip: false,
expiresAt: null,
};
+39
View File
@@ -0,0 +1,39 @@
import { expect, test } from "@playwright/test";
import { mockCoreApis } from "../../fixtures/api-mocks";
test.beforeEach(async ({ context, page }) => {
await context.clearCookies();
await mockCoreApis(page);
});
test("guest can skip splash, enter chat, and send a message", async ({ page }) => {
await page.goto("/splash");
await page.getByRole("button", { name: "Skip" }).click();
await expect(page).toHaveURL(/\/chat$/);
const messageInput = page.getByRole("textbox", { name: "Message" });
await expect(messageInput).toBeVisible();
await messageInput.fill("hello e2e");
await page.getByRole("button", { name: "Send message" }).click();
await expect(page.getByText("hello e2e")).toBeVisible();
await expect(page.getByText("Hello from the E2E boyfriend.")).toBeVisible();
});
test("subscription page renders mocked plans without touching real payment", async ({
page,
}) => {
await page.goto("/splash");
await page.getByRole("button", { name: "Skip" }).click();
await expect(page).toHaveURL(/\/chat$/);
await page.goto("/subscription?type=vip");
await expect(page.getByText("Subscribe to become the VIP Member")).toBeVisible();
await expect(page.getByText("Monthly")).toBeVisible();
await expect(page.getByText("Quarterly")).toBeVisible();
await expect(page.getByRole("button", { name: /Pay and Activiate/i })).toBeVisible();
});
+57
View File
@@ -0,0 +1,57 @@
import { expect, test } from "@playwright/test";
const apiBaseURL = process.env.E2E_API_BASE_URL;
test.describe("real backend smoke", () => {
test("splash page can be opened", async ({ page }) => {
await page.goto("/splash", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("button", { name: "Skip" })).toBeVisible();
await expect(
page.getByRole("button", { name: "Login with Facebook" }),
).toBeVisible();
});
test("protected subscription page redirects anonymous visitors safely", async ({
page,
}) => {
await page.goto("/subscription?type=vip", {
waitUntil: "domcontentloaded",
});
await expect(page.getByRole("button", { name: "Skip" })).toBeVisible();
await expect(page).toHaveURL(/\/splash(?:\?.*)?$/);
});
test("real payment plans API returns a usable plan list", async ({
request,
}) => {
test.skip(!apiBaseURL, "Set E2E_API_BASE_URL to test real backend APIs.");
const response = await request.get(`${apiBaseURL}/api/payment/plans`);
expect(response.ok()).toBe(true);
const body = await response.json();
expect(body.success).toBe(true);
expect(Array.isArray(body.data?.plans)).toBe(true);
expect(body.data.plans.length).toBeGreaterThan(0);
expect(body.data.plans[0]).toEqual(
expect.objectContaining({
plan_id: expect.any(String),
plan_name: expect.any(String),
amount_cents: expect.any(Number),
currency: expect.any(String),
}),
);
});
test("auth page exposes the social login entry without completing OAuth", async ({
page,
}) => {
await page.goto("/auth", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("button", { name: /facebook/i }),
).toBeVisible();
});
});