chore: import WonderQ project

This commit is contained in:
duanshuwen committed 2026-08-11 19:19:11 +08:00
commit f5a9e4403a
452 files changed
+34173

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
clearAuthSession,
getAuthSession,
isLoggedIn,
maskPhone,
requireLogin,
saveAuthSession,
} from "@/lib/auth";
function installUniStorageMock() {
const storage = new Map<string, unknown>();
const navigateTo = vi.fn();
(globalThis as unknown as { uni: unknown }).uni = {
getStorageSync: (key: string) => storage.get(key),
setStorageSync: (key: string, value: unknown) => storage.set(key, value),
removeStorageSync: (key: string) => storage.delete(key),
navigateTo,
};
return { navigateTo, storage };
}
describe("miniapp auth helpers", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("stores and clears the customer auth session", () => {
installUniStorageMock();
saveAuthSession({
token: "customer-token",
customer: { id: "customer-test", phoneMasked: "100****0000" },
});
expect(isLoggedIn()).toBe(true);
expect(getAuthSession()).toEqual({
token: "customer-token",
customer: { id: "customer-test", phoneMasked: "100****0000" },
});
clearAuthSession();
expect(isLoggedIn()).toBe(false);
expect(getAuthSession()).toBeNull();
});
it("redirects anonymous users to login with an encoded target", () => {
const { navigateTo } = installUniStorageMock();
const allowed = requireLogin("/pages/mine/index");
expect(allowed).toBe(false);
expect(navigateTo).toHaveBeenCalledWith({
url: "/pages/login/index?redirect=%2Fpages%2Fmine%2Findex",
});
});
it("masks phone numbers before rendering them on the client", () => {
expect(maskPhone("10000000000")).toBe("100****0000");
});
});
+381
View File
@@ -0,0 +1,381 @@
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("贵州黄果树");
});
});
+102
View File
@@ -0,0 +1,102 @@
import { describe, expect, it } from "vitest";
import type { Product } from "@/lib/types";
import {
defaultSearchFilterState,
filterAndSortSearchProducts,
normalizePriceInput,
parseTripDays,
} from "@/lib/searchFilters";
function createProduct(id: number, title: string, price: string): Product {
return {
id,
title,
price,
tags: [],
image: `/assets/test-${id}.jpg`,
};
}
describe("search filter helpers", () => {
const products = [
createProduct(1, "贵州黄果树 7天6晚精品小包团", "57500"),
createProduct(2, "荔波小七孔 15天及以上深度旅行", "112500"),
createProduct(3, "西江苗寨 4日轻松游", "36000"),
createProduct(4, "贵州定制旅行方案", "98000"),
];
it("parses trip days from route titles", () => {
expect(parseTripDays("贵州黄果树 7天6晚精品小包团")).toBe(7);
expect(parseTripDays("西江苗寨 4日轻松游")).toBe(4);
expect(parseTripDays("贵州定制旅行方案")).toBeNull();
});
it("normalizes price input values", () => {
expect(normalizePriceInput(" 57500 ")).toBe(57500);
expect(normalizePriceInput("")).toBeNull();
expect(normalizePriceInput("abc")).toBeNull();
});
it("filters products by applied price range", () => {
const results = filterAndSortSearchProducts(products, {
...defaultSearchFilterState,
priceMin: 50000,
priceMax: 100000,
});
expect(results.map((product) => product.id)).toEqual([1, 4]);
});
it("treats reversed price bounds as the same numeric range", () => {
const results = filterAndSortSearchProducts(products, {
...defaultSearchFilterState,
priceMin: 100000,
priceMax: 50000,
});
expect(results.map((product) => product.id)).toEqual([1, 4]);
});
it("sorts by price ascending and descending", () => {
const lowToHigh = filterAndSortSearchProducts(products, {
...defaultSearchFilterState,
sortKey: "priceAsc",
});
const highToLow = filterAndSortSearchProducts(products, {
...defaultSearchFilterState,
sortKey: "priceDesc",
});
expect(lowToHigh.map((product) => product.id)).toEqual([3, 1, 4, 2]);
expect(highToLow.map((product) => product.id)).toEqual([2, 4, 1, 3]);
});
it("keeps the recommended order for sales proxy sorting", () => {
const results = filterAndSortSearchProducts(products, {
...defaultSearchFilterState,
sortKey: "salesDesc",
});
expect(results.map((product) => product.id)).toEqual([1, 2, 3, 4]);
});
it("filters by exact days and fifteen days plus", () => {
const exact = filterAndSortSearchProducts(products, {
...defaultSearchFilterState,
duration: 4,
});
const longTrip = filterAndSortSearchProducts(products, {
...defaultSearchFilterState,
duration: "15plus",
});
expect(exact.map((product) => product.id)).toEqual([3]);
expect(longTrip.map((product) => product.id)).toEqual([2]);
});
it("clears filter conditions by using the default state", () => {
const results = filterAndSortSearchProducts(products, defaultSearchFilterState);
expect(results.map((product) => product.id)).toEqual([1, 2, 3, 4]);
});
});