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", }, { id: "maya-tan", slug: "maya", displayName: "Maya Tan", shortName: "Maya", cover: "maya.webp", }, { id: "nayeli-cervantes", slug: "nayeli", displayName: "Nayeli Cervantes", shortName: "Nayeli", cover: "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.cover}"]`); await expect(cover).toBeVisible(); await expect(page.getByText(character.displayName).last()).toBeVisible(); 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 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 albumRequest; await expect( page.getByRole("heading", { name: "Private albums" }), ).toBeVisible(); await expect(page.getByText(character.displayName).last()).toBeVisible(); await expect( page.getByRole("button", { name: `View locked collection with 8 images from ${character.displayName}`, }), ).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); }); } async function registerMultiRoleCommercialMocks(page: Page): Promise { 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]; await route.fulfill({ json: apiEnvelope({ creditBalance: 1000, items: [ { 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, }, ], }), }); }); }