fix(private-zone): hide incomplete albums and refresh Maya cover
Docker Image / Build and Push Docker Image (push) Successful in 2m12s
Docker Image / Build and Push Docker Image (push) Successful in 2m12s
This commit is contained in:
@@ -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}`,
|
});
|
||||||
}),
|
if (character.id === "maya-tan") {
|
||||||
).toBeVisible();
|
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 |
@@ -245,14 +245,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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user