import { describe, expect, it } from "vitest"; import { fallbackData, formatCampaignPrice, formatHotelPrice, formatProductCardPrice, getSearchProducts, normalizeApiProduct, normalizeDemandRecommendation, normalizeRouteSections, normalizeSiteConfig, productKey, stripTitle } from "@/lib/data"; describe("WonderQ shared data helpers", () => { it("keeps fallback products searchable by Guizhou aliases", () => { const results = getSearchProducts(fallbackData.products, "荔波小七孔"); expect(results.length).toBeGreaterThanOrEqual(2); expect(results.some((product) => product.title.includes("荔波") || product.tags.includes("荔波"))).toBe(true); }); it("normalizes API products to the miniapp product shape", () => { const product = normalizeApiProduct( { id: "api-product-42", sourceId: null, title: "测试贵州线路", subtitle: "后台短标题", priceAmount: 128000, tags: ["小包团"], coverImage: null, destination: { id: "dest-1", name: "贵阳" }, }, 3, ); expect(product.id).toBe(42); expect(product.price).toBe("128000"); expect(product.image).toBe("/assets/guizhou/libo-xiaoqikong.jpg"); expect(product.subtitle).toBe("后台短标题"); expect(product.destinationName).toBe("贵阳"); expect(productKey(product)).toBe("api-product-42"); }); it("formats product card price without wan conversion", () => { expect(formatProductCardPrice("57500")).toBe("¥57500起/人"); }); it("normalizes public site map image to app data", () => { const siteConfig = normalizeSiteConfig( { map: [{ id: "map-1", image: "https://cdn.example.test/guizhou-map.webp" }], }, fallbackData, ); expect(siteConfig.mapImage).toBe("https://cdn.example.test/guizhou-map.webp"); }); it("normalizes destination page hero and region modules", () => { const siteConfig = normalizeSiteConfig( { destinationHero: [ { id: "destination-hero-1", title: "后台主视觉标题", kicker: "后台小标题", image: "https://cdn.example.test/destination-hero.jpg", isActive: true, sortOrder: 0, }, ], destinationRegions: [ { id: "region-b", label: "黔南", keyword: "黔南荔波", spots: "荔波小七孔、茂兰", isActive: true, sortOrder: 2, }, { id: "region-a", label: "贵阳/安顺", keyword: "", spots: "甲秀楼、黄果树", isActive: true, sortOrder: 1, }, ], }, fallbackData, ); expect(siteConfig.destinationHero).toMatchObject({ id: "destination-hero-1", title: "后台主视觉标题", kicker: "后台小标题", image: "https://cdn.example.test/destination-hero.jpg", }); expect(siteConfig.destinationRegions.map((item) => item.label)).toEqual(["贵阳/安顺", "黔南"]); expect(siteConfig.destinationRegions[0].keyword).toBe("贵阳/安顺"); expect(siteConfig.destinationRegions[1].keyword).toBe("黔南荔波"); }); it("normalizes public hotel and vehicle modules", () => { const siteConfig = normalizeSiteConfig( { hotelGroups: [ { id: "hotel-b", title: "野奢酒店升级", description: "", image: "", isActive: true, sortOrder: 2, }, { id: "hotel-a", title: "经典酒店精选", description: "接口酒店描述", image: "https://cdn.example.test/hotel.jpg", coverImage: "https://cdn.example.test/hotel-cover.jpg", priceAmount: 68000, priceUnit: "起/晚", tags: ["温泉", "亲子"], status: "published", isActive: true, sortOrder: 1, }, ], vehicleOptions: [ { id: "vehicle-1", title: "7座舒适商务车", description: "接口用车描述", image: "https://cdn.example.test/vehicle.jpg", isActive: true, sortOrder: 0, }, ], }, fallbackData, ); expect(siteConfig.hotelGroups.map((item) => item.title)).toEqual(["经典酒店精选", "野奢酒店升级"]); expect(siteConfig.hotelGroups[0]).toMatchObject({ id: "hotel-a", description: "接口酒店描述", image: "https://cdn.example.test/hotel-cover.jpg", coverImage: "https://cdn.example.test/hotel-cover.jpg", priceAmount: 68000, priceUnit: "起/晚", tags: ["温泉", "亲子"], }); expect(siteConfig.hotelGroups[0].coverImage).toBe("https://cdn.example.test/hotel-cover.jpg"); expect(formatHotelPrice(siteConfig.hotelGroups[0].priceAmount, siteConfig.hotelGroups[0].priceUnit)).toBe("¥68000起/晚"); expect(formatHotelPrice(null, null)).toBe("¥107500起/晚"); expect(siteConfig.hotelGroups[1].image).toBe(fallbackData.hotelGroups[1].image); expect(siteConfig.vehicleOptions[0]).toMatchObject({ id: "vehicle-1", title: "7座舒适商务车", description: "接口用车描述", image: "https://cdn.example.test/vehicle.jpg", }); }); it("normalizes demand page modules and recommendation product order", () => { const siteConfig = normalizeSiteConfig( { demandHero: [ { id: "demand-hero-1", kicker: "后台 3 步", title: "后台需求标题", description: "后台需求说明", steps: ["提交", "沟通", "确认"], isActive: true, sortOrder: 0, }, ], demandFeatureCards: [ { id: "feature-b", title: "住宿", description: "后台住宿说明", isActive: true, sortOrder: 2, }, { id: "feature-a", title: "动线", description: "后台动线说明", isActive: true, sortOrder: 1, }, ], demandForm: [ { id: "form-1", destinationLabel: "想去哪里", destinationPlaceholder: "填写目的地", phoneLabel: "联系微信", phonePlaceholder: "微信号 / 手机号", noteLabel: "补充偏好", notePlaceholder: "人数、日期、酒店", submitLabel: "提交给管家", chips: ["荔波", "西江"], isActive: true, }, ], }, fallbackData, ); const products = [ normalizeApiProduct({ id: "product-a", sourceId: 1, title: "线路 A", tags: [] }, 0), normalizeApiProduct({ id: "product-b", sourceId: 2, title: "线路 B", tags: [] }, 1), ]; const recommendation = normalizeDemandRecommendation( [ { id: "recommendation-1", title: "后台热门", subtitle: "后台副文案", productIds: ["product-b", "product-a"], isActive: true, sortOrder: 0, }, ], products, fallbackData.demandRecommendation, ); expect(siteConfig.demandHero.title).toBe("后台需求标题"); expect(siteConfig.demandHero.steps).toEqual(["提交", "沟通", "确认"]); expect(siteConfig.demandFeatureCards.map((item) => item.title)).toEqual(["动线", "住宿"]); expect(siteConfig.demandForm).toMatchObject({ destinationLabel: "想去哪里", submitLabel: "提交给管家", chips: ["荔波", "西江"], }); expect(recommendation.title).toBe("后台热门"); expect(recommendation.products.map((product) => product.title)).toEqual(["线路 B", "线路 A"]); }); it("falls back to local demand page data when config is missing or productIds do not match", () => { const siteConfig = normalizeSiteConfig({}, fallbackData); const recommendation = normalizeDemandRecommendation( [ { id: "recommendation-empty", title: "无匹配推荐", productIds: ["missing-product"], isActive: true, }, ], fallbackData.products, fallbackData.demandRecommendation, ); expect(siteConfig.demandHero).toBe(fallbackData.demandHero); expect(siteConfig.demandFeatureCards).toBe(fallbackData.demandFeatureCards); expect(siteConfig.demandForm).toBe(fallbackData.demandForm); expect(recommendation).toBe(fallbackData.demandRecommendation); }); it("normalizes public campaigns to home deal cards", () => { const siteConfig = normalizeSiteConfig( { campaigns: [ { id: "campaign-1", slug: "classic", title: "经典打卡特惠", description: "接口活动描述", coverImage: "https://cdn.example.test/campaign.jpg", priceAmount: 57500, priceUnit: "起/人", }, ], }, fallbackData, ); expect(siteConfig.campaigns).toHaveLength(1); expect(siteConfig.campaigns[0]).toMatchObject({ id: "campaign-1", title: "经典打卡特惠", campaignType: "经典打卡特惠", image: "https://cdn.example.test/campaign.jpg", description: "接口活动描述", priceAmount: 57500, priceUnit: "起/人", tags: ["经典首游", "小包团优惠"], }); expect(formatCampaignPrice(siteConfig.campaigns[0].priceAmount, siteConfig.campaigns[0].priceUnit, siteConfig.campaigns[0].priceText)).toBe("¥57500起/人"); }); it("normalizes public route sections by productIds order", () => { const products = [ normalizeApiProduct( { id: "product-a", sourceId: 1, title: "线路 A", tags: [], }, 0, ), normalizeApiProduct( { id: "product-b", sourceId: 2, title: "线路 B", tags: [], }, 1, ), ]; const sections = normalizeRouteSections( [ { id: "route-section-001", title: "后台精选线路", subtitle: "后台副文案", productIds: ["product-b", "product-a"], isActive: true, }, ], products, fallbackData.routeSections, ); expect(sections).toHaveLength(1); expect(sections[0].title).toBe("后台精选线路"); expect(sections[0].products.map((product) => product.title)).toEqual(["线路 B", "线路 A"]); }); it("skips inactive route sections and sections without matched products", () => { const products = [ normalizeApiProduct( { id: "product-visible", sourceId: 1, title: "线路 A", tags: [], }, 0, ), ]; const sections = normalizeRouteSections( [ { id: "route-section-inactive", title: "停用分组", productIds: ["product-visible"], isActive: false, }, { id: "route-section-empty", title: "无匹配商品分组", productIds: ["missing-product"], isActive: true, }, { id: "route-section-active", title: "有效分组", productIds: ["product-visible"], isActive: true, }, ], products, fallbackData.routeSections, ); expect(sections).toHaveLength(1); expect(sections[0].id).toBe("route-section-active"); expect(sections[0].products.map((product) => product.title)).toEqual(["线路 A"]); }); it("falls back to local route sections when public route sections are empty", () => { expect(normalizeRouteSections([], fallbackData.products, fallbackData.routeSections)).toBe(fallbackData.routeSections); }); it("strips source brackets from product display titles", () => { expect(stripTitle("【贵州】黄果树·荔波小七孔5日")).toBe("贵州黄果树"); }); });