test(e2e): add paid media flow coverage
This commit is contained in:
@@ -5,11 +5,14 @@ import {
|
||||
chatSendResponse,
|
||||
createPaymentOrderResponse,
|
||||
createChatSendResponse,
|
||||
createPaidImageHistoryResponse,
|
||||
createWeeklyLimitChatSendResponse,
|
||||
e2eEmailUser,
|
||||
emptyChatHistoryResponse,
|
||||
emailLoginResponse,
|
||||
guestLoginResponse,
|
||||
paidImageChatSendResponse,
|
||||
paidImageUnlockHistoryResponse,
|
||||
paidPaymentOrderStatusResponse,
|
||||
paymentPlansResponse,
|
||||
userEntitlementsResponse,
|
||||
@@ -18,20 +21,27 @@ import {
|
||||
|
||||
interface MockCoreApisOptions {
|
||||
chatLimitTriggerAt?: number;
|
||||
paidImageFlow?: boolean;
|
||||
}
|
||||
|
||||
export async function mockCoreApis(
|
||||
page: Page,
|
||||
options: MockCoreApisOptions = {},
|
||||
): Promise<void> {
|
||||
const { chatLimitTriggerAt } = options;
|
||||
const { chatLimitTriggerAt, paidImageFlow = false } = options;
|
||||
let sendCount = 0;
|
||||
let paidImageRequested = false;
|
||||
let paidImageUnlocked = false;
|
||||
|
||||
await page.route("**/api/auth/guest", async (route) => {
|
||||
await route.fulfill({ json: apiEnvelope(guestLoginResponse) });
|
||||
});
|
||||
|
||||
await page.route("**/api/auth/login", async (route) => {
|
||||
if (paidImageFlow) {
|
||||
paidImageRequested = true;
|
||||
}
|
||||
|
||||
await route.fulfill({ json: apiEnvelope(emailLoginResponse) });
|
||||
});
|
||||
|
||||
@@ -48,18 +58,62 @@ export async function mockCoreApis(
|
||||
});
|
||||
|
||||
await page.route("**/api/chat/history**", async (route) => {
|
||||
await route.fulfill({ json: apiEnvelope(emptyChatHistoryResponse) });
|
||||
const response =
|
||||
paidImageFlow && paidImageRequested
|
||||
? createPaidImageHistoryResponse(paidImageUnlocked)
|
||||
: emptyChatHistoryResponse;
|
||||
|
||||
await route.fulfill({ json: apiEnvelope(response) });
|
||||
});
|
||||
|
||||
await page.route("**/api/chat/send", async (route) => {
|
||||
sendCount += 1;
|
||||
const requestBody = route.request().postDataJSON() as
|
||||
| { message?: unknown }
|
||||
| undefined;
|
||||
const message =
|
||||
typeof requestBody?.message === "string" ? requestBody.message : "";
|
||||
|
||||
const response =
|
||||
chatLimitTriggerAt != null && sendCount >= chatLimitTriggerAt
|
||||
? createWeeklyLimitChatSendResponse(sendCount, chatLimitTriggerAt)
|
||||
: chatLimitTriggerAt != null
|
||||
? createChatSendResponse(sendCount)
|
||||
: chatSendResponse;
|
||||
paidImageFlow && message.includes("给我发图片")
|
||||
? paidImageChatSendResponse
|
||||
: chatLimitTriggerAt != null && sendCount >= chatLimitTriggerAt
|
||||
? createWeeklyLimitChatSendResponse(sendCount, chatLimitTriggerAt)
|
||||
: chatLimitTriggerAt != null
|
||||
? createChatSendResponse(sendCount)
|
||||
: chatSendResponse;
|
||||
|
||||
if (paidImageFlow && message.includes("给我发图片")) {
|
||||
paidImageRequested = true;
|
||||
paidImageUnlocked = false;
|
||||
}
|
||||
|
||||
await route.fulfill({ json: apiEnvelope(response) });
|
||||
});
|
||||
|
||||
await page.route("**/api/chat/unlock-history", async (route) => {
|
||||
if (paidImageFlow && paidImageRequested) {
|
||||
paidImageUnlocked = true;
|
||||
}
|
||||
|
||||
const response =
|
||||
paidImageFlow && paidImageRequested
|
||||
? paidImageUnlockHistoryResponse
|
||||
: {
|
||||
unlocked: true,
|
||||
reason: "no_locked_messages",
|
||||
totalLocked: 0,
|
||||
unlockedCount: 0,
|
||||
privateCount: 0,
|
||||
imageCount: 0,
|
||||
voiceCount: 0,
|
||||
requiredCredits: 0,
|
||||
currentCredits: 0,
|
||||
remainingCredits: 0,
|
||||
shortfallCredits: 0,
|
||||
costsByMessage: {},
|
||||
messageIds: [],
|
||||
};
|
||||
|
||||
await route.fulfill({ json: apiEnvelope(response) });
|
||||
});
|
||||
@@ -73,6 +127,10 @@ export async function mockCoreApis(
|
||||
});
|
||||
|
||||
await page.route("**/api/payment/order-status**", async (route) => {
|
||||
if (paidImageFlow && paidImageRequested) {
|
||||
paidImageUnlocked = true;
|
||||
}
|
||||
|
||||
await route.fulfill({ json: apiEnvelope(paidPaymentOrderStatusResponse) });
|
||||
});
|
||||
|
||||
|
||||
@@ -127,6 +127,86 @@ export function createWeeklyLimitChatSendResponse(
|
||||
};
|
||||
}
|
||||
|
||||
export const paidImageMessageId = "msg_photo_paywall_001";
|
||||
export const paidImageUrl =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
|
||||
|
||||
export const paidImageChatSendResponse = {
|
||||
reply: "I can show you that photo after you activate VIP.",
|
||||
audioUrl: "",
|
||||
messageId: paidImageMessageId,
|
||||
isGuest: true,
|
||||
timestamp: 1_782_180_725_000,
|
||||
image: {
|
||||
type: "elio_schedule",
|
||||
url: paidImageUrl,
|
||||
},
|
||||
lockDetail: {
|
||||
locked: true,
|
||||
showContent: true,
|
||||
showUpgrade: true,
|
||||
reason: "image",
|
||||
hint: "Activate VIP to unlock the full photo.",
|
||||
detail: null,
|
||||
},
|
||||
};
|
||||
|
||||
export function createPaidImageHistoryResponse(unlocked: boolean) {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
type: "image",
|
||||
content: unlocked
|
||||
? ""
|
||||
: "I can show you that photo after you activate VIP.",
|
||||
id: paidImageMessageId,
|
||||
created_at: "2026-06-30T00:00:00.000Z",
|
||||
audioUrl: null,
|
||||
image: {
|
||||
type: "elio_schedule",
|
||||
url: paidImageUrl,
|
||||
},
|
||||
lockDetail: unlocked
|
||||
? {
|
||||
locked: false,
|
||||
showContent: true,
|
||||
showUpgrade: false,
|
||||
reason: null,
|
||||
hint: null,
|
||||
detail: null,
|
||||
}
|
||||
: paidImageChatSendResponse.lockDetail,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
isVip: unlocked,
|
||||
privateFreeLimit: 0,
|
||||
privateUsedToday: 0,
|
||||
privateCanViewFree: false,
|
||||
};
|
||||
}
|
||||
|
||||
export const paidImageUnlockHistoryResponse = {
|
||||
unlocked: true,
|
||||
reason: "ok",
|
||||
totalLocked: 1,
|
||||
unlockedCount: 1,
|
||||
privateCount: 0,
|
||||
imageCount: 1,
|
||||
voiceCount: 0,
|
||||
requiredCredits: 0,
|
||||
currentCredits: 0,
|
||||
remainingCredits: 0,
|
||||
shortfallCredits: 0,
|
||||
costsByMessage: {
|
||||
[paidImageMessageId]: 0,
|
||||
},
|
||||
messageIds: [paidImageMessageId],
|
||||
};
|
||||
|
||||
export const paymentPlansResponse = {
|
||||
plans: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
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, {
|
||||
paidImageFlow: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("guest can unlock a paid image after email login and subscription payment", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterChatFromSplash(page);
|
||||
await dismissChatInterruptions(page);
|
||||
|
||||
const messageInput = page.getByRole("textbox", { name: "Message" });
|
||||
await expect(messageInput).toBeVisible({ timeout: 10_000 });
|
||||
await messageInput.fill("给我发图片");
|
||||
|
||||
const sendRequestPromise = page.waitForRequest("**/api/chat/send");
|
||||
await messageInput.press("Enter");
|
||||
|
||||
const sendRequest = await sendRequestPromise;
|
||||
expect(sendRequest.postDataJSON()).toMatchObject({
|
||||
message: "给我发图片",
|
||||
});
|
||||
|
||||
const paidImageButton = page.getByRole("button", {
|
||||
name: "Open image in fullscreen",
|
||||
}).last();
|
||||
await expect(paidImageButton).toBeVisible();
|
||||
|
||||
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();
|
||||
|
||||
await lockedImageDialog
|
||||
.getByRole("button", { name: "Unlock high-definition large image" })
|
||||
.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/auth\?redirect=/);
|
||||
expect(new URL(page.url()).searchParams.get("redirect")).toBe(
|
||||
"/subscription?type=vip&returnTo=chat",
|
||||
);
|
||||
|
||||
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");
|
||||
|
||||
const loginRequestPromise = page.waitForRequest("**/api/auth/login");
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
|
||||
const loginRequest = await loginRequestPromise;
|
||||
expect(loginRequest.postDataJSON()).toMatchObject({
|
||||
email: "chanwillian0@gmail.com",
|
||||
password: "12345678",
|
||||
isTestAccount: true,
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/subscription\?type=vip&returnTo=chat$/);
|
||||
|
||||
const createOrderRequestPromise = page.waitForRequest(
|
||||
"**/api/payment/create-order",
|
||||
);
|
||||
const orderStatusRequestPromise = page.waitForRequest(
|
||||
"**/api/payment/order-status**",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: /Pay and Activ/i }).click();
|
||||
|
||||
const createOrderRequest = await createOrderRequestPromise;
|
||||
expect(createOrderRequest.postDataJSON()).toMatchObject({
|
||||
planId: "vip_monthly",
|
||||
payChannel: "stripe",
|
||||
});
|
||||
await orderStatusRequestPromise;
|
||||
|
||||
const paymentSuccessDialog = page.getByRole("alertdialog", {
|
||||
name: "Payment successful",
|
||||
});
|
||||
await expect(paymentSuccessDialog).toBeVisible();
|
||||
await paymentSuccessDialog.getByRole("button", { name: "OK" }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/chat/image/${paidImageMessageId}$`));
|
||||
});
|
||||
|
||||
async function enterChatFromSplash(page: import("@playwright/test").Page) {
|
||||
await page.goto("/splash");
|
||||
|
||||
const skipButton = page.getByRole("button", { name: "Skip" });
|
||||
await expect(skipButton).toBeVisible({ timeout: 10_000 });
|
||||
await expect(skipButton).toBeEnabled();
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
await skipButton.click();
|
||||
try {
|
||||
await expect(page).toHaveURL(/\/chat$/, { timeout: 3_000 });
|
||||
return;
|
||||
} catch {
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
await expect(page).toHaveURL(/\/chat$/);
|
||||
}
|
||||
|
||||
async function dismissChatInterruptions(page: import("@playwright/test").Page) {
|
||||
const laterButton = page.getByRole("button", { name: "Later" });
|
||||
if (await laterButton.isVisible().catch(() => false)) {
|
||||
await laterButton.click();
|
||||
}
|
||||
|
||||
const cancelButton = page.getByRole("button", { name: "Cancel" });
|
||||
if (await cancelButton.isVisible().catch(() => false)) {
|
||||
await cancelButton.click();
|
||||
}
|
||||
|
||||
const okButton = page.getByRole("button", { name: "OK" });
|
||||
if (await okButton.isVisible().catch(() => false)) {
|
||||
await okButton.click();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user