fix(private-zone): hide incomplete albums and refresh Maya cover
Docker Image / Build and Push Docker Image (push) Successful in 1m59s

This commit is contained in:
Codex
2026-07-23 11:47:22 +08:00
parent 0357fbcaff
commit 770ce6b8fd
9 changed files with 74 additions and 16 deletions
+27 -6
View File
@@ -99,11 +99,16 @@ for (const character of characters) {
page.getByRole("heading", { name: "Private albums" }), page.getByRole("heading", { name: "Private albums" }),
).toBeVisible(); ).toBeVisible();
await expect(page.getByText(character.displayName).last()).toBeVisible(); await expect(page.getByText(character.displayName).last()).toBeVisible();
await expect( const albumButton = page.getByRole("button", {
page.getByRole("button", {
name: `View locked collection with 8 images from ${character.displayName}`, name: `View locked collection with 8 images from ${character.displayName}`,
}), });
).toBeVisible(); 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 ({ test(`${character.displayName} receives a character-scoped Tip catalog`, async ({
@@ -133,6 +138,19 @@ for (const character of characters) {
}); });
} }
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> { async function registerMultiRoleCommercialMocks(page: Page): Promise<void> {
await page.route("**/api/private-zone/albums**", async (route) => { await page.route("**/api/private-zone/albums**", async (route) => {
const url = new URL(route.request().url()); const url = new URL(route.request().url());
@@ -140,10 +158,13 @@ async function registerMultiRoleCommercialMocks(page: Page): Promise<void> {
const character = const character =
characters.find((candidate) => candidate.id === characterId) ?? characters[0]; characters.find((candidate) => candidate.id === characterId) ?? characters[0];
const hasCompleteAlbum = character.id !== "maya-tan";
await route.fulfill({ await route.fulfill({
json: apiEnvelope({ json: apiEnvelope({
creditBalance: 1000, creditBalance: 1000,
items: [ pendingImageCount: hasCompleteAlbum ? 0 : 7,
hasIncompleteContent: !hasCompleteAlbum,
items: hasCompleteAlbum ? [
{ {
albumId: `album-${character.id}`, albumId: `album-${character.id}`,
title: `${character.shortName} private photos`, title: `${character.shortName} private photos`,
@@ -163,7 +184,7 @@ async function registerMultiRoleCommercialMocks(page: Page): Promise<void> {
publishedAt: "2026-07-22T10:00:00+08:00", publishedAt: "2026-07-22T10:00:00+08:00",
lockDetail: { locked: true }, lockDetail: { locked: true },
}, },
], ] : [],
}), }),
}); });
}); });
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 581 KiB

@@ -257,14 +257,6 @@ export function PrivateZoneScreen() {
<StatusCard message="Loading private albums..." /> <StatusCard message="Loading private albums..." />
) : null} ) : null}
{!state.isLoading && state.items.length === 0 && !state.errorMessage ? (
<StatusCard
message="No private albums yet."
actionLabel="Refresh"
onAction={() => dispatch({ type: "PrivateZoneRefresh" })}
/>
) : null}
<div className={styles.timeline}> <div className={styles.timeline}>
{state.items.map((album, index) => ( {state.items.map((album, index) => (
<PrivateAlbumCard <PrivateAlbumCard
@@ -67,6 +67,20 @@ describe("Private Album schema models", () => {
expect(response).not.toHaveProperty("packageOptions"); expect(response).not.toHaveProperty("packageOptions");
}); });
it("keeps incomplete-content metadata without exposing pending images", () => {
const response = PrivateAlbumsResponseSchema.parse({
items: [],
creditBalance: 100,
pendingImageCount: 7,
hasIncompleteContent: true,
pendingImages: [{ url: COVER_URL }],
});
expect(response.pendingImageCount).toBe(7);
expect(response.hasIncompleteContent).toBe(true);
expect(response).not.toHaveProperty("pendingImages");
});
it("parses an unlock response with all album images", () => { it("parses an unlock response with all album images", () => {
const response = PrivateAlbumUnlockResponseSchema.parse({ const response = PrivateAlbumUnlockResponseSchema.parse({
albumId: ALBUM_ID, albumId: ALBUM_ID,
@@ -1,12 +1,18 @@
import { z } from "zod"; import { z } from "zod";
import { arrayOrEmpty, numberOrZero } from "../../nullable-defaults"; import {
arrayOrEmpty,
booleanOrFalse,
numberOrZero,
} from "../../nullable-defaults";
import { PrivateAlbumSchema } from "../private_album"; import { PrivateAlbumSchema } from "../private_album";
export const PrivateAlbumsResponseSchema = z export const PrivateAlbumsResponseSchema = z
.object({ .object({
items: arrayOrEmpty(PrivateAlbumSchema), items: arrayOrEmpty(PrivateAlbumSchema),
creditBalance: numberOrZero, creditBalance: numberOrZero,
pendingImageCount: numberOrZero,
hasIncompleteContent: booleanOrFalse,
}) })
.readonly(); .readonly();
@@ -41,6 +41,21 @@ describe("private zone album flow", () => {
actor.stop(); actor.stop();
}); });
it("keeps incomplete album metadata without creating a visible album", async () => {
const actor = await loadPrivateZone({
loadResponse: makeAlbumsResponse({
items: [],
pendingImageCount: 7,
hasIncompleteContent: true,
}),
});
expect(actor.getSnapshot().context.items).toEqual([]);
expect(actor.getSnapshot().context.pendingImageCount).toBe(7);
expect(actor.getSnapshot().context.hasIncompleteContent).toBe(true);
actor.stop();
});
it("moves to failed when the album actor rejects", async () => { it("moves to failed when the album actor rejects", async () => {
const actor = createActor( const actor = createActor(
createTestPrivateZoneMachine({ createTestPrivateZoneMachine({
+2
View File
@@ -15,6 +15,8 @@ export function applyAlbumsResponse(
return { return {
items: response.items, items: response.items,
creditBalance: response.creditBalance, creditBalance: response.creditBalance,
pendingImageCount: response.pendingImageCount,
hasIncompleteContent: response.hasIncompleteContent,
errorMessage: null, errorMessage: null,
}; };
} }
@@ -16,6 +16,8 @@ export interface PrivateZoneContextState {
status: string; status: string;
items: MachineContext["items"]; items: MachineContext["items"];
creditBalance: number; creditBalance: number;
pendingImageCount: number;
hasIncompleteContent: boolean;
errorMessage: string | null; errorMessage: string | null;
unlockingAlbumId: string | null; unlockingAlbumId: string | null;
unlockErrorMessage: string | null; unlockErrorMessage: string | null;
@@ -70,6 +72,8 @@ function selectPrivateZoneState(
status: String(state.value), status: String(state.value),
items: state.context.items, items: state.context.items,
creditBalance: state.context.creditBalance, creditBalance: state.context.creditBalance,
pendingImageCount: state.context.pendingImageCount,
hasIncompleteContent: state.context.hasIncompleteContent,
errorMessage: state.context.errorMessage, errorMessage: state.context.errorMessage,
unlockingAlbumId: state.context.unlockingAlbumId, unlockingAlbumId: state.context.unlockingAlbumId,
unlockErrorMessage: state.context.unlockErrorMessage, unlockErrorMessage: state.context.unlockErrorMessage,
@@ -13,6 +13,8 @@ export interface PrivateZoneState {
characterId: string; characterId: string;
items: readonly PrivateAlbum[]; items: readonly PrivateAlbum[];
creditBalance: number; creditBalance: number;
pendingImageCount: number;
hasIncompleteContent: boolean;
errorMessage: string | null; errorMessage: string | null;
unlockingAlbumId: string | null; unlockingAlbumId: string | null;
unlockErrorMessage: string | null; unlockErrorMessage: string | null;
@@ -28,6 +30,8 @@ export function createInitialPrivateZoneState(
characterId, characterId,
items: [], items: [],
creditBalance: 0, creditBalance: 0,
pendingImageCount: 0,
hasIncompleteContent: false,
errorMessage: null, errorMessage: null,
unlockingAlbumId: null, unlockingAlbumId: null,
unlockErrorMessage: null, unlockErrorMessage: null,