chore: import WonderQ project
This commit is contained in:
commit
f5a9e4403a
452 files changed
+34173
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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("贵州黄果树");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user