From 854a7498b7cf0bca1e4824f1d178bffa2c143b29 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 1 Jul 2026 16:01:06 +0800 Subject: [PATCH] test(e2e): add paid image insufficient credits flow --- e2e/fixtures/api-mocks.ts | 46 +++++---- e2e/fixtures/test-data.ts | 18 ++++ .../image-unlock-insufficient-credits.spec.ts | 93 +++++++++++++++++++ 3 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 e2e/specs/mock/image-unlock-insufficient-credits.spec.ts diff --git a/e2e/fixtures/api-mocks.ts b/e2e/fixtures/api-mocks.ts index 3ef0f862..7f3c7e39 100644 --- a/e2e/fixtures/api-mocks.ts +++ b/e2e/fixtures/api-mocks.ts @@ -11,6 +11,7 @@ import { emptyChatHistoryResponse, emailLoginResponse, guestLoginResponse, + insufficientCreditsUnlockImageResponse, insufficientCreditsUnlockVoiceResponse, paidImageChatSendResponse, paidImageUnlockHistoryResponse, @@ -24,6 +25,7 @@ import { interface MockCoreApisOptions { chatLimitTriggerAt?: number; paidImageFlow?: boolean; + paidImageInsufficientCreditsFlow?: boolean; paidVoiceInsufficientCreditsFlow?: boolean; } @@ -34,6 +36,7 @@ export async function mockCoreApis( const { chatLimitTriggerAt, paidImageFlow = false, + paidImageInsufficientCreditsFlow = false, paidVoiceInsufficientCreditsFlow = false, } = options; let sendCount = 0; @@ -68,6 +71,8 @@ export async function mockCoreApis( const response = paidVoiceInsufficientCreditsFlow ? paidVoiceHistoryResponse + : paidImageInsufficientCreditsFlow + ? createPaidImageHistoryResponse(false) : paidImageFlow && paidImageRequested ? createPaidImageHistoryResponse(paidImageUnlocked) : emptyChatHistoryResponse; @@ -128,25 +133,28 @@ export async function mockCoreApis( }); await page.route("**/api/chat/unlock-private", async (route) => { - const response = paidVoiceInsufficientCreditsFlow - ? insufficientCreditsUnlockVoiceResponse - : { - unlocked: true, - content: "Unlocked message", - reason: "ok", - creditBalance: 100, - creditsCharged: 0, - requiredCredits: 0, - shortfallCredits: 0, - lockDetail: { - locked: false, - showContent: true, - showUpgrade: false, - reason: null, - hint: null, - detail: null, - }, - }; + const response = + paidVoiceInsufficientCreditsFlow + ? insufficientCreditsUnlockVoiceResponse + : paidImageInsufficientCreditsFlow + ? insufficientCreditsUnlockImageResponse + : { + unlocked: true, + content: "Unlocked message", + reason: "ok", + creditBalance: 100, + creditsCharged: 0, + requiredCredits: 0, + shortfallCredits: 0, + lockDetail: { + locked: false, + showContent: true, + showUpgrade: false, + reason: null, + hint: null, + detail: null, + }, + }; await route.fulfill({ json: apiEnvelope(response) }); }); diff --git a/e2e/fixtures/test-data.ts b/e2e/fixtures/test-data.ts index 0dadb344..575b2a1e 100644 --- a/e2e/fixtures/test-data.ts +++ b/e2e/fixtures/test-data.ts @@ -261,6 +261,24 @@ export const insufficientCreditsUnlockVoiceResponse = { }, }; +export const insufficientCreditsUnlockImageResponse = { + unlocked: false, + content: "", + reason: "insufficient_balance", + creditBalance: 3, + creditsCharged: 0, + requiredCredits: 30, + shortfallCredits: 27, + lockDetail: { + locked: true, + showContent: true, + showUpgrade: true, + reason: "image", + hint: "Unlock this high-definition image with credits.", + detail: null, + }, +}; + export const paymentPlansResponse = { plans: [ { diff --git a/e2e/specs/mock/image-unlock-insufficient-credits.spec.ts b/e2e/specs/mock/image-unlock-insufficient-credits.spec.ts new file mode 100644 index 00000000..1b108a27 --- /dev/null +++ b/e2e/specs/mock/image-unlock-insufficient-credits.spec.ts @@ -0,0 +1,93 @@ +import { expect, test } from "@playwright/test"; + +import { mockCoreApis } from "../../fixtures/api-mocks"; +import { paidImageMessageId } from "../../fixtures/test-data"; + +test.beforeEach(async ({ baseURL, context, page }) => { + await context.clearCookies(); + + const client = await context.newCDPSession(page); + const origins = new Set([ + new URL(baseURL ?? "http://127.0.0.1:3000").origin, + "http://127.0.0.1:3000", + "http://localhost:3000", + ]); + for (const origin of origins) { + await client + .send("Storage.clearDataForOrigin", { + origin, + storageTypes: "all", + }) + .catch(() => {}); + } + + await mockCoreApis(page, { + paidImageInsufficientCreditsFlow: true, + }); +}); + +test("user sees insufficient credits when unlocking a paid image from fullscreen and can navigate to subscription", async ({ + page, +}) => { + await signInWithEmailAndOpenChat(page); + + const paidImageButton = page.getByRole("button", { + name: "Open image in fullscreen", + }); + await expect(paidImageButton).toBeVisible({ timeout: 10_000 }); + + await paidImageButton.click(); + await expect(page).toHaveURL(new RegExp(`/chat/image/${paidImageMessageId}$`)); + + const lockedImageDialog = page.getByRole("dialog", { + name: "Locked fullscreen image", + }); + await expect(lockedImageDialog).toBeVisible(); + + const unlockRequestPromise = page.waitForRequest( + "**/api/chat/unlock-private", + ); + await lockedImageDialog + .getByRole("button", { name: "Unlock high-definition large image" }) + .click(); + + const unlockRequest = await unlockRequestPromise; + expect(unlockRequest.method()).toBe("POST"); + expect(unlockRequest.postDataJSON()).toMatchObject({ + messageId: paidImageMessageId, + }); + + const insufficientCreditsDialog = page.getByRole("dialog", { + name: "Not enough credits", + }); + await expect(insufficientCreditsDialog).toBeVisible(); + await expect(insufficientCreditsDialog.getByText("Balance")).toBeVisible(); + await expect(insufficientCreditsDialog.getByText("Required")).toBeVisible(); + await expect(insufficientCreditsDialog.getByText("Still needed")).toBeVisible(); + + await insufficientCreditsDialog + .getByRole("button", { name: "Top up now" }) + .click(); + + await expect(page).toHaveURL(/\/subscription\?type=vip&returnTo=chat$/); +}); + +async function signInWithEmailAndOpenChat( + page: import("@playwright/test").Page, +) { + await page.goto("/auth"); + + await page.getByRole("button", { name: /Other Sign In Options/i }).click(); + await expect( + page.getByRole("dialog", { name: /Other sign-in options/i }), + ).toBeVisible(); + + await page.getByRole("button", { name: "Email Sign In" }).click(); + await expect(page.getByPlaceholder("Email")).toHaveValue( + "chanwillian0@gmail.com", + ); + await expect(page.getByPlaceholder("Password")).toHaveValue("12345678"); + + await page.getByRole("button", { name: "Login" }).click(); + await expect(page).toHaveURL(/\/chat$/); +}