fix(multi-role): enable Maya and Nayeli commercial flows
This commit is contained in:
@@ -31,5 +31,5 @@ test("user can email login from other sign-in options and return to chat", async
|
|||||||
expect(["desktop", "android"]).toContain(loginRequest?.postDataJSON().platform);
|
expect(["desktop", "android"]).toContain(loginRequest?.postDataJSON().platform);
|
||||||
|
|
||||||
await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible();
|
await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Profile" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Save CozSweet" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ test("user can log out from the profile after email login", async ({
|
|||||||
await expect(page).toHaveURL(/\/auth(?:\?.*)?$/);
|
await expect(page).toHaveURL(/\/auth(?:\?.*)?$/);
|
||||||
await switchToEmailSignIn(page);
|
await switchToEmailSignIn(page);
|
||||||
await submitEmailLogin(page, { expectedUrl: defaultCharacterChatUrl });
|
await submitEmailLogin(page, { expectedUrl: defaultCharacterChatUrl });
|
||||||
await expect(page.getByRole("button", { name: "Profile" })).toBeVisible();
|
await page.getByRole("link", { name: "Back to home" }).click();
|
||||||
|
await expect(page).toHaveURL(
|
||||||
|
new RegExp(defaultCharacterSplashPath.replace("?", "\\?") + "$"),
|
||||||
|
);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Profile" }).click();
|
await page.getByRole("button", { name: "Menu" }).click();
|
||||||
await expect(page).toHaveURL(
|
await expect(page).toHaveURL(
|
||||||
new RegExp(defaultCharacterProfilePath.replace("?", "\\?") + "$"),
|
new RegExp(defaultCharacterProfilePath.replace("?", "\\?") + "$"),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
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 Zoom`, async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const albumRequest = page.waitForRequest((request) => {
|
||||||
|
const url = new URL(request.url());
|
||||||
|
return (
|
||||||
|
url.pathname === "/api/private-zoom/albums" &&
|
||||||
|
url.searchParams.get("characterId") === character.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/characters/${character.slug}/private-zoom`);
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
await page.route("**/api/private-zoom/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-zoom/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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 351 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
@@ -96,7 +96,7 @@ const CHARACTER_PROFILES: readonly CharacterProfile[] = Object.freeze([
|
|||||||
},
|
},
|
||||||
assets: {
|
assets: {
|
||||||
avatar: "/images/avatar/maya.png",
|
avatar: "/images/avatar/maya.png",
|
||||||
cover: "/images/cover/maya.png",
|
cover: "/images/cover/maya.webp",
|
||||||
chatBackground: "/images/chat/bg-chatpage.png",
|
chatBackground: "/images/chat/bg-chatpage.png",
|
||||||
privateZoomBanner: "/images/private-zoom/banner/maya.png",
|
privateZoomBanner: "/images/private-zoom/banner/maya.png",
|
||||||
},
|
},
|
||||||
@@ -125,7 +125,7 @@ const CHARACTER_PROFILES: readonly CharacterProfile[] = Object.freeze([
|
|||||||
},
|
},
|
||||||
assets: {
|
assets: {
|
||||||
avatar: "/images/avatar/nayeli.png",
|
avatar: "/images/avatar/nayeli.png",
|
||||||
cover: "/images/cover/nayeli.png",
|
cover: "/images/cover/nayeli.webp",
|
||||||
chatBackground: "/images/chat/bg-chatpage.png",
|
chatBackground: "/images/chat/bg-chatpage.png",
|
||||||
privateZoomBanner: "/images/private-zoom/banner/nayeli.png",
|
privateZoomBanner: "/images/private-zoom/banner/nayeli.png",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user