199 lines
6.6 KiB
TypeScript
199 lines
6.6 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 { e2eEmailUser } from "@e2e/fixtures/data/auth";
|
|
import { chatCharactersResponse } from "@e2e/fixtures/data/character";
|
|
import {
|
|
emptyChatHistoryResponse,
|
|
emptyChatPreviewsResponse,
|
|
} from "@e2e/fixtures/data/chat";
|
|
import { paymentPlansResponse } from "@e2e/fixtures/data/payment";
|
|
import { userEntitlementsResponse } from "@e2e/fixtures/data/user";
|
|
import {
|
|
defaultCharacterChatPath,
|
|
defaultCharacterSplashPath,
|
|
seedEmailSession,
|
|
} from "@e2e/fixtures/helpers/auth";
|
|
|
|
const previewDirectory = path.join(
|
|
process.cwd(),
|
|
".codex",
|
|
"artifacts",
|
|
"frontend-preview",
|
|
"2026-07-22",
|
|
"favorite-menu",
|
|
);
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await registerFavoriteMenuMocks(page);
|
|
});
|
|
|
|
test("favorite entry and three-tab Menu navigation render on the real pages", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const mobile = testInfo.project.name.includes("mobile");
|
|
await page.setViewportSize(
|
|
mobile ? { width: 390, height: 844 } : { width: 1440, height: 900 },
|
|
);
|
|
const browserErrors = collectBrowserErrors(page);
|
|
|
|
await seedEmailSession(page);
|
|
await page.goto(defaultCharacterChatPath);
|
|
|
|
await expect(page.getByRole("button", { name: "Save CozSweet" })).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: /First recharge offer, 50% off/i }),
|
|
).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Profile" })).toHaveCount(0);
|
|
await savePreview(page, `${mobile ? "mobile" : "desktop"}-chat.png`);
|
|
|
|
await page.goto(defaultCharacterSplashPath);
|
|
await expect(page.getByRole("button", { name: "Save CozSweet" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
|
|
const navigation = page.getByRole("navigation", {
|
|
name: "Primary navigation",
|
|
});
|
|
await expect(navigation).toContainText("Chat");
|
|
await expect(navigation).toContainText("Elio Private Zone");
|
|
await expect(navigation).toContainText("Menu");
|
|
await savePreview(page, `${mobile ? "mobile" : "desktop"}-splash.png`);
|
|
|
|
await page.goto("/characters/elio/private-zone");
|
|
await expect(page.getByRole("button", { name: "Save CozSweet" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
|
|
await savePreview(
|
|
page,
|
|
`${mobile ? "mobile" : "desktop"}-private-zone.png`,
|
|
);
|
|
|
|
await page.getByRole("button", { name: "Menu" }).click();
|
|
await expect(page.getByRole("heading", { name: "Menu" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Save CozSweet" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Menu" })).toHaveAttribute(
|
|
"aria-current",
|
|
"page",
|
|
);
|
|
await savePreview(page, `${mobile ? "mobile" : "desktop"}-menu.png`);
|
|
|
|
if (mobile) {
|
|
await page.evaluate(() => {
|
|
localStorage.setItem("cozsweet:favorite_entry", "1");
|
|
});
|
|
await page.goto(defaultCharacterChatPath);
|
|
await expect(page.getByRole("button", { name: "Download" })).toBeVisible();
|
|
await savePreview(page, "mobile-android-external-chat-download.png");
|
|
|
|
await page.addInitScript(() => {
|
|
Object.defineProperty(navigator, "userAgent", {
|
|
configurable: true,
|
|
value:
|
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1",
|
|
});
|
|
});
|
|
await page.goto(defaultCharacterChatPath);
|
|
await expect(page.getByRole("button", { name: "Saved" })).toBeVisible();
|
|
await savePreview(page, "mobile-ios-external-chat-saved.png");
|
|
}
|
|
|
|
expect(browserErrors.filter(isFeatureRuntimeError)).toEqual([]);
|
|
});
|
|
|
|
async function registerFavoriteMenuMocks(page: Page): Promise<void> {
|
|
await mockCoreApis(page);
|
|
await page.route("**/api/payment/plans**", async (route) => {
|
|
await route.fulfill({
|
|
json: apiEnvelope({
|
|
...paymentPlansResponse,
|
|
isFirstRecharge: true,
|
|
}),
|
|
});
|
|
});
|
|
await page.route("**/api/private-zone/albums**", async (route) => {
|
|
await route.fulfill({
|
|
json: apiEnvelope({ items: [], creditBalance: 80 }),
|
|
});
|
|
});
|
|
await page.context().route("**/api/**", async (route) => {
|
|
const pathname = new URL(route.request().url()).pathname;
|
|
if (pathname === "/api/auth/session") {
|
|
await route.fulfill({
|
|
json: {
|
|
expires: "2099-12-31T23:59:59.000Z",
|
|
user: { email: null, image: null, name: null },
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
if (pathname === "/api/characters") {
|
|
await route.fulfill({ json: apiEnvelope(chatCharactersResponse) });
|
|
return;
|
|
}
|
|
if (pathname === "/api/user/profile") {
|
|
await route.fulfill({ json: apiEnvelope(e2eEmailUser) });
|
|
return;
|
|
}
|
|
if (pathname === "/api/user/entitlements") {
|
|
await route.fulfill({ json: apiEnvelope(userEntitlementsResponse) });
|
|
return;
|
|
}
|
|
if (pathname === "/api/chat/history") {
|
|
await route.fulfill({ json: apiEnvelope(emptyChatHistoryResponse) });
|
|
return;
|
|
}
|
|
if (pathname === "/api/chat/previews") {
|
|
await route.fulfill({ json: apiEnvelope(emptyChatPreviewsResponse) });
|
|
return;
|
|
}
|
|
if (pathname === "/api/payment/plans") {
|
|
await route.fulfill({
|
|
json: apiEnvelope({
|
|
...paymentPlansResponse,
|
|
isFirstRecharge: true,
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
if (pathname === "/api/payment/vip-status") {
|
|
await route.fulfill({
|
|
json: apiEnvelope({ isVip: false, expiresAt: null }),
|
|
});
|
|
return;
|
|
}
|
|
if (pathname === "/api/private-zone/albums") {
|
|
await route.fulfill({
|
|
json: apiEnvelope({ items: [], creditBalance: 80 }),
|
|
});
|
|
return;
|
|
}
|
|
await route.fallback();
|
|
});
|
|
}
|
|
|
|
function collectBrowserErrors(page: Page): string[] {
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") errors.push(message.text());
|
|
});
|
|
return errors;
|
|
}
|
|
|
|
function isFeatureRuntimeError(message: string): boolean {
|
|
const isKnownLocalPreviewDependencyError =
|
|
message.includes("[ApiLoggingInterceptor]") ||
|
|
message.includes("[next-auth][error][CLIENT_FETCH_ERROR]") ||
|
|
message.includes("[Result] Result.wrap caught exception");
|
|
return !isKnownLocalPreviewDependencyError;
|
|
}
|
|
|
|
async function savePreview(page: Page, fileName: string): Promise<void> {
|
|
if (process.env.FRONTEND_PREVIEW_SCREENSHOTS !== "1") return;
|
|
await page.screenshot({
|
|
path: path.join(previewDirectory, fileName),
|
|
animations: "disabled",
|
|
});
|
|
}
|