d5b7a1f36c
Docker Image / Build and Push Docker Image (push) Successful in 2m17s
(cherry picked from commit a2cf62f78b)
265 lines
7.9 KiB
TypeScript
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 albums" }),
|
|
).toBeVisible();
|
|
await expect(page.getByRole("tab")).toHaveText(["Albums", "Moments"]);
|
|
await expect(page.getByRole("tab", { name: "Albums" })).toHaveAttribute(
|
|
"aria-selected",
|
|
"true",
|
|
);
|
|
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 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,
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
}
|