Files
cozsweet-frontend-nextjs/e2e/specs/mock/multi-role-commercial.spec.ts
T
2026-07-27 16:11:24 +08:00

265 lines
7.9 KiB
TypeScript

import path from "node:path";
import { expect, test, type Page } from "@playwright/test";
import { mockCoreApis } from "@e2e/fixtures/api-mocks";
import { apiEnvelope } from "@e2e/fixtures/data/common";
import { clearBrowserState } from "@e2e/fixtures/test-helpers";
const characters = [
{
id: "elio",
slug: "elio",
displayName: "Elio Silvestri",
shortName: "Elio",
cover: "elio.png",
splashCover: "elio.png",
},
{
id: "maya-tan",
slug: "maya",
displayName: "Maya Tan",
shortName: "Maya",
cover: "maya.webp",
splashCover: "maya-home.webp",
},
{
id: "nayeli-cervantes",
slug: "nayeli",
displayName: "Nayeli Cervantes",
shortName: "Nayeli",
cover: "nayeli.webp",
splashCover: "nayeli.webp",
},
] as const;
const previewDirectory = path.join(
process.cwd(),
".codex",
"artifacts",
"frontend-preview",
"2026-07-22",
"multi-role-commercial",
);
test.beforeEach(async ({ baseURL, context, page }) => {
await clearBrowserState(context, page, baseURL);
await mockCoreApis(page);
await registerMultiRoleCommercialMocks(page);
});
for (const character of characters) {
test(`${character.displayName} homepage renders its assigned cover`, async ({
page,
}, testInfo) => {
const mobile = testInfo.project.name.includes("mobile");
await page.setViewportSize(
mobile ? { width: 390, height: 844 } : { width: 1440, height: 900 },
);
await page.goto(`/characters/${character.slug}/splash`);
const cover = page.locator(`img[src*="${character.splashCover}"]`);
await expect(cover).toBeVisible();
await expect(
page.getByRole("navigation", { name: "Primary navigation" }),
).toContainText("Private Zone");
await expect
.poll(() =>
cover.evaluate((image: HTMLImageElement) =>
image.complete ? image.naturalWidth : 0,
),
)
.toBeGreaterThan(0);
if (
process.env.FRONTEND_PREVIEW_SCREENSHOTS === "1" &&
character.id !== "elio"
) {
await page.screenshot({
path: path.join(
previewDirectory,
`${character.slug}-${mobile ? "mobile-390x844" : "desktop-1440x900"}.png`,
),
animations: "disabled",
});
}
});
test(`${character.displayName} can open a character-scoped Private Zone`, async ({
page,
}) => {
const momentsRequest = page.waitForRequest((request) => {
const url = new URL(request.url());
return (
url.pathname === "/api/private-zone/posts" &&
url.searchParams.get("characterId") === character.id
);
});
const albumRequest = page.waitForRequest((request) => {
const url = new URL(request.url());
return (
url.pathname === "/api/private-zone/albums" &&
url.searchParams.get("characterId") === character.id
);
});
await page.goto(`/characters/${character.slug}/private-zone`);
await Promise.all([momentsRequest, albumRequest]);
await expect(
page.getByRole("heading", { name: "Private moments" }),
).toBeVisible();
await page.getByRole("tab", { name: "Albums" }).click();
await expect(
page.getByRole("heading", { name: "Private albums" }),
).toBeVisible();
await expect(page.getByText(character.displayName).last()).toBeVisible();
const albumButton = page.getByRole("button", {
name: `View locked collection with 8 images from ${character.displayName}`,
});
if (character.id === "maya-tan") {
await expect(albumButton).toHaveCount(0);
await expect(page.getByRole("button", { name: "Refresh" })).toHaveCount(0);
await expect(page.getByText("No private albums yet.")).toHaveCount(0);
} else {
await expect(albumButton).toBeVisible();
}
});
test(`${character.displayName} receives a character-scoped Tip catalog`, async ({
page,
}) => {
const catalogRequest = page.waitForRequest((request) => {
const url = new URL(request.url());
return (
url.pathname === "/api/payment/gift-products" &&
url.searchParams.get("characterId") === character.id
);
});
await page.goto(`/characters/${character.slug}/tip`);
await catalogRequest;
await expect(
page.getByRole("heading", { name: `Buy ${character.shortName} a coffee` }),
).toBeVisible();
await expect(page.getByRole("radio", { name: /Small Coffee/ })).toBeChecked();
await expect(
page.getByRole("button", { name: "Order and Buy" }),
).toBeEnabled();
await expect(
page.getByText("This character does not have any gifts available yet."),
).toHaveCount(0);
});
}
test("a real Private Zone request failure still offers Retry", async ({
page,
}) => {
await page.route("**/api/private-zone/albums**", async (route) => {
await route.fulfill({ status: 503, json: { message: "Albums unavailable" } });
});
await page.goto("/characters/maya/private-zone");
await page.getByRole("tab", { name: "Albums" }).click();
await expect(page.getByRole("button", { name: "Retry" })).toBeVisible();
await expect(page.getByRole("button", { name: "Refresh" })).toHaveCount(0);
});
async function registerMultiRoleCommercialMocks(page: Page): Promise<void> {
await page.route("**/api/private-zone/posts**", async (route) => {
const url = new URL(route.request().url());
const characterId = url.searchParams.get("characterId") ?? "elio";
await route.fulfill({
json: apiEnvelope({
characterId,
items: [],
nextCursor: null,
hasMore: false,
creditBalance: 1000,
currency: "credits",
}),
});
});
await page.route("**/api/private-zone/albums**", async (route) => {
const url = new URL(route.request().url());
const characterId = url.searchParams.get("characterId") ?? "elio";
const character =
characters.find((candidate) => candidate.id === characterId) ?? characters[0];
const hasCompleteAlbum = character.id !== "maya-tan";
await route.fulfill({
json: apiEnvelope({
creditBalance: 1000,
pendingImageCount: hasCompleteAlbum ? 0 : 7,
hasIncompleteContent: !hasCompleteAlbum,
items: hasCompleteAlbum ? [
{
albumId: `album-${character.id}`,
title: `${character.shortName} private photos`,
content: null,
previewText: "Unlock this private album",
imageCount: 8,
images: [
{
url: `/images/private-zone/banner/${character.slug}.png`,
locked: true,
index: 0,
},
],
locked: true,
unlocked: false,
unlockCost: 320,
publishedAt: "2026-07-22T10:00:00+08:00",
lockDetail: { locked: true },
},
] : [],
}),
});
});
await page.route("**/api/payment/gift-products**", async (route) => {
const url = new URL(route.request().url());
const characterId = url.searchParams.get("characterId") ?? "elio";
const planPrefix =
characterId === "elio"
? "tip"
: `tip_${characterId.replaceAll("-", "_")}`;
await route.fulfill({
json: apiEnvelope({
characterId,
categories: [
{
category: "coffee",
name: "Coffee",
productCount: 1,
imageUrl: null,
},
],
plans: [
{
planId: `${planPrefix}_coffee_usd_4_99`,
planName: "Small Coffee",
orderType: "tip",
tipType: "coffee_small",
category: "coffee",
characterId,
description: "A character-scoped coffee gift",
imageUrl: null,
amountCents: 499,
currency: "USD",
autoRenew: false,
isFirstRechargeOffer: false,
firstRechargeDiscountPercent: 0,
promotionType: null,
},
],
}),
});
});
}