Files
Codex a2cf62f78b
Docker Image / Build and Push Docker Image (push) Successful in 2m7s
feat(private-zone): default to albums tab
2026-07-27 19:05:58 +08:00

165 lines
5.1 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { mockCoreApis } from "@e2e/fixtures/api-mocks";
import { apiEnvelope } from "@e2e/fixtures/data/common";
import {
clearBrowserState,
seedEmailSession,
} from "@e2e/fixtures/test-helpers";
const postId = "11111111-1111-1111-1111-111111111111";
let unlocked = false;
test.beforeEach(async ({ baseURL, context, page }) => {
unlocked = false;
await clearBrowserState(context, page, baseURL);
await mockCoreApis(page);
await registerPrivateZoneVideoMocks(page);
});
test("Private Zone defaults to Albums and reveals video after switching to Moments", async ({
page,
}) => {
await seedEmailSession(page);
const listRequest = page.waitForRequest((request) => {
const url = new URL(request.url());
return (
url.pathname === "/api/private-zone/posts" &&
url.searchParams.get("characterId") === "maya-tan"
);
});
await page.goto("/characters/maya/private-zone");
await listRequest;
await expect(
page.getByRole("heading", { name: "Private albums" }),
).toBeVisible();
await expect(page.getByText("Maya archive")).toBeVisible();
await page.getByRole("tab", { name: "Moments" }).click();
await expect(
page.getByRole("heading", { name: "Private moments" }),
).toBeVisible();
await expect(page.getByText("Only for you.")).toBeVisible();
await expect(page.locator("video")).toHaveCount(0);
await page.getByRole("button", { name: "Unlock for 100 credits" }).click();
const dialog = page.getByRole("alertdialog");
await expect(dialog).toContainText("100 credits");
const unlockRequest = page.waitForRequest(
`**/api/private-zone/posts/${postId}/unlock`,
);
await dialog.getByRole("button", { name: "Unlock", exact: true }).click();
expect((await unlockRequest).postDataJSON()).toEqual({ expectedCost: 100 });
const video = page.locator("video");
await expect(video).toHaveCount(1);
await expect(video.locator("source")).toHaveAttribute("src", /video\.mp4/);
await expect(page.getByRole("alertdialog")).toHaveCount(0);
});
test("Albums render first and Moments remain available behind the second tab", async ({ page }) => {
await seedEmailSession(page);
await page.goto("/characters/maya/private-zone");
const tabs = page.getByRole("tab");
await expect(tabs).toHaveText(["Albums", "Moments"]);
await expect(page.getByRole("tab", { name: "Albums" })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(
page.getByRole("heading", { name: "Private albums" }),
).toBeVisible();
await expect(page.getByText("Maya archive")).toBeVisible();
await page.getByRole("tab", { name: "Moments" }).click();
await expect(page.getByRole("tab", { name: "Moments" })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(
page.getByRole("heading", { name: "Private moments" }),
).toBeVisible();
});
async function registerPrivateZoneVideoMocks(page: Page): Promise<void> {
await page.route("**/api/private-zone/posts**", async (route) => {
const url = new URL(route.request().url());
await route.fulfill({
json: apiEnvelope({
characterId: url.searchParams.get("characterId") ?? "maya-tan",
items: [videoPost(unlocked)],
nextCursor: null,
hasMore: false,
creditBalance: unlocked ? 150 : 250,
currency: "credits",
}),
});
});
// Playwright evaluates page routes in reverse registration order. Register
// the specific mutation last so the broader list route cannot intercept it.
await page.route("**/api/private-zone/posts/*/unlock", async (route) => {
unlocked = true;
await route.fulfill({
json: apiEnvelope({
postId,
reason: "ok",
unlocked: true,
creditsCharged: 100,
requiredCredits: 100,
currentCredits: 150,
shortfallCredits: 0,
creditBalance: 150,
post: videoPost(true),
}),
});
});
await page.route("**/api/private-zone/albums**", async (route) => {
await route.fulfill({
json: apiEnvelope({
items: [
{
albumId: "album-maya",
title: "Maya archive",
content: null,
previewText: "Private photos",
imageCount: 1,
images: [{ url: "/images/private-zone/banner/maya.png", locked: true, index: 0 }],
locked: true,
unlocked: false,
unlockCost: 80,
publishedAt: "2026-07-24T10:00:00+00:00",
lockDetail: { locked: true },
},
],
creditBalance: 250,
pendingImageCount: 0,
hasIncompleteContent: false,
}),
});
});
}
function videoPost(isUnlocked: boolean) {
return {
postId,
characterId: "maya-tan",
caption: "Only for you.",
posterUrl: "/images/private-zone/banner/maya.png",
mediaType: "video",
videoUrl: isUnlocked ? "/test-video.mp4?token=short" : null,
videoMimeType: "video/mp4",
videoSizeBytes: 1024,
durationSeconds: 18,
unlockCostCredits: 100,
currency: "credits",
locked: !isUnlocked,
unlocked: isUnlocked,
availableForNewUnlock: true,
publishedAt: "2026-07-24T10:00:00+00:00",
};
}