import { describe, expect, it } from "vitest"; import { buildPackagePreview, createPreviewAssetLoader, parsePreviewItemIds, } from "./catalog-package-preview-model"; const page = (id: number, parentId = -1, overrides = {}) => ({ id, parentId, caption: `Page ${id}`, enabled: "1" as const, visible: "1" as const, iconImage: 0, minRank: 1, clubOnly: "0" as const, vipOnly: "0" as const, orderNum: id, pageLayout: "default_3x3", ...overrides, }); const profile = { playerMode: true, rank: 1, club: false, vip: false }; describe("package preview access", () => { it("honors restrictions on every included ancestor", () => { const result = buildPackagePreview( { pages: [page(1, -1, { minRank: 5 }), page(2, 1)], offers: [] }, profile, "", ); expect(result.pages).toEqual([]); expect(result.restrictedCount).toBe(2); }); it("handles hidden/disabled ancestors and club membership", () => { for (const restriction of [ { visible: "0" }, { enabled: "0" }, { clubOnly: "1" }, { vipOnly: "1" }, ]) { expect( buildPackagePreview( { pages: [page(1, -1, restriction), page(2, 1)], offers: [] }, profile, "", ).pages, ).toEqual([]); } expect( buildPackagePreview( { pages: [page(1, -1, { clubOnly: "1" })], offers: [] }, { ...profile, vip: true }, "", ).pages, ).toHaveLength(1); }); it("rejects cycles without hanging and reports external subtree boundaries", () => { const result = buildPackagePreview( { pages: [page(1, 2), page(2, 1), page(3, 999)], offers: [] }, profile, "", ); expect(result.pages.map((p) => p.id)).toEqual([3]); expect(result.invalidCount).toBe(2); expect(result.externalParents).toBe(true); }); it("bounds oversized snapshots before traversing", () => { const result = buildPackagePreview( { pages: Array.from({ length: 500 }, (_, i) => page(i + 1)), offers: [] }, profile, "", ); expect(result.pages).toHaveLength(200); expect(result.truncated).toBe(true); }); it("preserves parent navigation when only a child offer matches", () => { const result = buildPackagePreview( { pages: [page(1), page(2, 1)], offers: [ { id: 1, pageId: 2, catalogName: "Sofa", itemIds: "4;5", costCredits: 2, costPoints: 0, pointsType: 0, amount: 1, }, ], }, profile, "sofa", ); expect(result.pages.map((p) => p.id)).toEqual([1, 2]); expect(result.matchingPageIds).toEqual([2]); }); it("parses bundle IDs strictly without treating partial numbers as IDs", () => { expect(parsePreviewItemIds("2; 3,4;2;abc;5x;-1;0")).toEqual([2, 3, 4]); }); }); describe("preview asset requests", () => { it("ignores older responses after changing selection", async () => { const resolves: ((value: number[]) => void)[] = []; const loader = createPreviewAssetLoader( () => new Promise((resolve) => resolves.push(resolve)), ); const seen: number[][] = []; const first = loader.run([1], (value) => seen.push(value)); const second = loader.run([2], (value) => seen.push(value)); resolves[1]([2]); await second; resolves[0]([1]); await first; expect(seen).toEqual([[2]]); }); it("does not publish after unmount/cancellation", async () => { let finish = (_value: number[]) => {}; const loader = createPreviewAssetLoader( () => new Promise((resolve) => { finish = resolve; }), ); const seen: number[][] = []; const pending = loader.run([1], (value) => seen.push(value)); loader.cancel(); finish([1]); await pending; expect(seen).toEqual([]); }); }); describe("offer membership access", () => { it("filters Club offers separately from their public category and permits VIP", () => { const snapshot = { pages: [page(1)], offers: [ { id: 1, pageId: 1, catalogName: "Club chair", clubOnly: "1" as const, itemIds: "2", costCredits: 1, costPoints: 0, pointsType: 0, amount: 1, }, ], }; const guest = buildPackagePreview(snapshot, profile, ""); expect(guest.pages).toHaveLength(1); expect(guest.offers).toHaveLength(0); expect(guest.restrictedOfferCounts[1]).toBe(1); expect( buildPackagePreview(snapshot, { ...profile, vip: true }, "").offers, ).toHaveLength(1); expect( buildPackagePreview(snapshot, { ...profile, playerMode: false }, "") .offers, ).toHaveLength(1); }); });