feat(private-room): connect moments feed
Docker Image / Build and Push Docker Image (push) Successful in 9m29s
Docker Image / Build and Push Docker Image (push) Successful in 9m29s
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createActor, fromPromise, waitFor } from "xstate";
|
||||
|
||||
import {
|
||||
PrivateRoomMomentsResponse,
|
||||
PrivateRoomUnlockResponse,
|
||||
} from "@/data/dto/private-room";
|
||||
import { privateRoomMachine } from "@/stores/private-room/private-room-machine";
|
||||
|
||||
const baseMoment = {
|
||||
momentId: "schedule:91",
|
||||
source: "elio_schedules",
|
||||
sourceId: "91",
|
||||
characterId: "elio",
|
||||
author: {
|
||||
id: "elio",
|
||||
name: "Elio Silvestri",
|
||||
avatarUrl: null,
|
||||
},
|
||||
createdAt: "2026-07-01T08:11:35+00:00",
|
||||
publishedAt: "2026-07-01T08:11:35+00:00",
|
||||
timeText: "7 days ago",
|
||||
title: "Paris morning",
|
||||
content: null,
|
||||
text: null,
|
||||
textPreview: "Unlock to view private room photos",
|
||||
mediaCount: 1,
|
||||
images: [
|
||||
{
|
||||
url: null,
|
||||
type: "image",
|
||||
locked: true,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
locked: true,
|
||||
unlocked: false,
|
||||
unlockCost: 40,
|
||||
unlockCostPerImage: 40,
|
||||
requiredCredits: 40,
|
||||
currency: "credits",
|
||||
lockDetail: {
|
||||
locked: true,
|
||||
showContent: false,
|
||||
showUpgrade: true,
|
||||
reason: "private_room_moment",
|
||||
hint: "40 credits · Unlock photo",
|
||||
actionLabel: "Unlock",
|
||||
type: "private_room_moment",
|
||||
requiredCredits: 40,
|
||||
currentCredits: 100,
|
||||
shortfallCredits: 0,
|
||||
mediaCount: 1,
|
||||
unlockCostPerImage: 40,
|
||||
},
|
||||
};
|
||||
|
||||
function makeMomentsResponse(
|
||||
overrides: Partial<Parameters<typeof PrivateRoomMomentsResponse.from>[0]> = {},
|
||||
): PrivateRoomMomentsResponse {
|
||||
return PrivateRoomMomentsResponse.from({
|
||||
profile: {
|
||||
characterId: "elio",
|
||||
displayName: "Elio Silvestri",
|
||||
authorName: "Elio Silvestri",
|
||||
avatarUrl: null,
|
||||
coverUrl: null,
|
||||
title: "Elio Private room",
|
||||
subtitle: "Join me, unlock my private room",
|
||||
},
|
||||
items: [baseMoment],
|
||||
nextCursor: null,
|
||||
hasMore: false,
|
||||
creditBalance: 100,
|
||||
unlockCostDefault: 40,
|
||||
unlockCostPerImage: 40,
|
||||
currency: "credits",
|
||||
source: "elio_schedules",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function makeUnlockResponse(
|
||||
overrides: Partial<Parameters<typeof PrivateRoomUnlockResponse.from>[0]> = {},
|
||||
): PrivateRoomUnlockResponse {
|
||||
return PrivateRoomUnlockResponse.from({
|
||||
...baseMoment,
|
||||
locked: false,
|
||||
unlocked: true,
|
||||
images: [
|
||||
{
|
||||
url: "https://example.supabase.co/storage/v1/object/public/elio-schedules/photo.jpg",
|
||||
type: "image",
|
||||
locked: false,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
reason: "ok",
|
||||
creditsCharged: 40,
|
||||
creditBalance: 60,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function createTestMachine(
|
||||
options: {
|
||||
firstPage?: PrivateRoomMomentsResponse;
|
||||
morePage?: PrivateRoomMomentsResponse;
|
||||
unlockResponse?: PrivateRoomUnlockResponse;
|
||||
} = {},
|
||||
) {
|
||||
return privateRoomMachine.provide({
|
||||
actors: {
|
||||
loadMoments: fromPromise(async () => options.firstPage ?? makeMomentsResponse()),
|
||||
loadMoreMoments: fromPromise(async () =>
|
||||
options.morePage ??
|
||||
makeMomentsResponse({
|
||||
items: [
|
||||
{
|
||||
...baseMoment,
|
||||
momentId: "schedule:92",
|
||||
sourceId: "92",
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
nextCursor: null,
|
||||
}),
|
||||
),
|
||||
unlockMoment: fromPromise(async () =>
|
||||
options.unlockResponse ?? makeUnlockResponse(),
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("privateRoomMachine", () => {
|
||||
it("loads the first page", async () => {
|
||||
const actor = createActor(createTestMachine()).start();
|
||||
|
||||
actor.send({ type: "PrivateRoomInit" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
|
||||
const context = actor.getSnapshot().context;
|
||||
expect(context.profile?.displayName).toBe("Elio Silvestri");
|
||||
expect(context.items).toHaveLength(1);
|
||||
expect(context.creditBalance).toBe(100);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("appends more moments without overwriting existing items", async () => {
|
||||
const actor = createActor(
|
||||
createTestMachine({
|
||||
firstPage: makeMomentsResponse({
|
||||
hasMore: true,
|
||||
nextCursor: "cursor-1",
|
||||
}),
|
||||
}),
|
||||
).start();
|
||||
|
||||
actor.send({ type: "PrivateRoomInit" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
|
||||
actor.send({ type: "PrivateRoomLoadMore" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
|
||||
expect(actor.getSnapshot().context.items.map((item) => item.momentId)).toEqual([
|
||||
"schedule:91",
|
||||
"schedule:92",
|
||||
]);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("replaces a locked moment after unlock success", async () => {
|
||||
const actor = createActor(createTestMachine()).start();
|
||||
|
||||
actor.send({ type: "PrivateRoomInit" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
|
||||
actor.send({
|
||||
type: "PrivateRoomUnlockRequested",
|
||||
momentId: "schedule:91",
|
||||
});
|
||||
actor.send({ type: "PrivateRoomUnlockConfirmed" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
|
||||
const context = actor.getSnapshot().context;
|
||||
expect(context.items[0]?.unlocked).toBe(true);
|
||||
expect(context.items[0]?.images[0]?.url).toContain("https://");
|
||||
expect(context.unlockSuccessNonce).toBe(1);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("exposes a paywall request for insufficient credits", async () => {
|
||||
const actor = createActor(
|
||||
createTestMachine({
|
||||
unlockResponse: makeUnlockResponse({
|
||||
...baseMoment,
|
||||
reason: "insufficient_credits",
|
||||
locked: true,
|
||||
unlocked: false,
|
||||
creditBalance: 10,
|
||||
currentCredits: 10,
|
||||
shortfallCredits: 30,
|
||||
}),
|
||||
}),
|
||||
).start();
|
||||
|
||||
actor.send({ type: "PrivateRoomInit" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
|
||||
actor.send({
|
||||
type: "PrivateRoomUnlockRequested",
|
||||
momentId: "schedule:91",
|
||||
});
|
||||
actor.send({ type: "PrivateRoomUnlockConfirmed" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
|
||||
const context = actor.getSnapshot().context;
|
||||
expect(context.items[0]?.unlocked).toBe(false);
|
||||
expect(context.unlockPaywallRequest).toMatchObject({
|
||||
momentId: "schedule:91",
|
||||
reason: "insufficient_credits",
|
||||
shortfallCredits: 30,
|
||||
});
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("refreshes and keeps an unlock error when the cost changes", async () => {
|
||||
const actor = createActor(
|
||||
createTestMachine({
|
||||
unlockResponse: makeUnlockResponse({
|
||||
...baseMoment,
|
||||
reason: "cost_changed",
|
||||
locked: true,
|
||||
unlocked: false,
|
||||
}),
|
||||
}),
|
||||
).start();
|
||||
|
||||
actor.send({ type: "PrivateRoomInit" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
|
||||
actor.send({
|
||||
type: "PrivateRoomUnlockRequested",
|
||||
momentId: "schedule:91",
|
||||
});
|
||||
actor.send({ type: "PrivateRoomUnlockConfirmed" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
|
||||
expect(actor.getSnapshot().context.unlockErrorMessage).toBe(
|
||||
"Unlock price changed. Please confirm again.",
|
||||
);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user