refactor: drop unused local search catalog
This commit is contained in:
@@ -1,417 +0,0 @@
|
||||
import type { SearchCatalogData } from "@/lib/search/search-catalog"
|
||||
import { searchCatalog } from "@/lib/search/search-catalog"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
const createdAt = "2025-01-01T00:00:00.000Z"
|
||||
|
||||
const players: SearchCatalogData["players"] = [
|
||||
{
|
||||
id: "1006",
|
||||
user: {
|
||||
id: "2006",
|
||||
username: "u1006",
|
||||
nickname: "Winter",
|
||||
avatar: "/avatars/u1006.png",
|
||||
role: "player",
|
||||
createdAt,
|
||||
},
|
||||
rating: 4.8,
|
||||
totalOrders: 120,
|
||||
completionRate: 0.98,
|
||||
status: "available",
|
||||
games: ["王者荣耀"],
|
||||
services: [
|
||||
{
|
||||
id: "s-1006-wzry",
|
||||
playerId: "1006",
|
||||
gameId: "g-wzry",
|
||||
gameName: "王者荣耀",
|
||||
title: "王者荣耀陪练",
|
||||
description: "",
|
||||
price: 30,
|
||||
unit: "局",
|
||||
availability: ["weekends"],
|
||||
},
|
||||
],
|
||||
shopId: "3002",
|
||||
gender: true,
|
||||
tags: ["moba"],
|
||||
},
|
||||
{
|
||||
id: "1007",
|
||||
user: {
|
||||
id: "2007",
|
||||
username: "u1007",
|
||||
nickname: "u7",
|
||||
avatar: "/avatars/u1007.png",
|
||||
role: "player",
|
||||
createdAt,
|
||||
},
|
||||
rating: 4.0,
|
||||
totalOrders: 20,
|
||||
completionRate: 0.9,
|
||||
status: "busy",
|
||||
games: ["英雄联盟"],
|
||||
services: [
|
||||
{
|
||||
id: "s-1007-lol",
|
||||
playerId: "1007",
|
||||
gameId: "g-lol",
|
||||
gameName: "英雄联盟",
|
||||
title: "英雄联盟陪练",
|
||||
description: "",
|
||||
price: 18,
|
||||
unit: "局",
|
||||
availability: ["weekdays"],
|
||||
},
|
||||
],
|
||||
shopId: "3003",
|
||||
gender: true,
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: "1008",
|
||||
user: {
|
||||
id: "2008",
|
||||
username: "u1008",
|
||||
nickname: "Ace",
|
||||
avatar: "/avatars/u1008.png",
|
||||
role: "player",
|
||||
createdAt,
|
||||
},
|
||||
rating: 4.7,
|
||||
totalOrders: 80,
|
||||
completionRate: 0.97,
|
||||
status: "available",
|
||||
games: ["CS2"],
|
||||
services: [
|
||||
{
|
||||
id: "s-1008-cs2",
|
||||
playerId: "1008",
|
||||
gameId: "g-cs2",
|
||||
gameName: "CS2",
|
||||
title: "CS2代练",
|
||||
description: "",
|
||||
price: 10,
|
||||
unit: "局",
|
||||
availability: ["weekends"],
|
||||
},
|
||||
],
|
||||
shopId: "3001",
|
||||
gender: false,
|
||||
tags: ["fps"],
|
||||
},
|
||||
{
|
||||
id: "1009",
|
||||
user: {
|
||||
id: "2009",
|
||||
username: "u1009",
|
||||
nickname: "u9",
|
||||
avatar: "/avatars/u1009.png",
|
||||
role: "player",
|
||||
createdAt,
|
||||
},
|
||||
rating: 3.5,
|
||||
totalOrders: 5,
|
||||
completionRate: 0.85,
|
||||
status: "offline",
|
||||
games: ["英雄联盟"],
|
||||
services: [
|
||||
{
|
||||
id: "s-1009-lol",
|
||||
playerId: "1009",
|
||||
gameId: "g-lol",
|
||||
gameName: "英雄联盟",
|
||||
title: "英雄联盟陪练",
|
||||
description: "",
|
||||
price: 12,
|
||||
unit: "局",
|
||||
availability: ["weekends"],
|
||||
},
|
||||
],
|
||||
shopId: "3003",
|
||||
gender: true,
|
||||
tags: [],
|
||||
},
|
||||
]
|
||||
|
||||
const shops: SearchCatalogData["shops"] = [
|
||||
{
|
||||
id: "3001",
|
||||
owner: {
|
||||
id: "4001",
|
||||
username: "owner3001",
|
||||
nickname: "Owner 3001",
|
||||
avatar: "/avatars/owner3001.png",
|
||||
role: "owner",
|
||||
createdAt,
|
||||
},
|
||||
name: "CS2 Hub",
|
||||
description: "",
|
||||
rating: "4.6",
|
||||
totalOrders: 300,
|
||||
playerCount: 1,
|
||||
commissionType: "fixed",
|
||||
commissionValue: "0",
|
||||
allowMultiShop: false,
|
||||
allowIndependentOrders: false,
|
||||
dispatchMode: "manual",
|
||||
announcements: [],
|
||||
templateConfig: { sections: [] },
|
||||
},
|
||||
{
|
||||
id: "3002",
|
||||
owner: {
|
||||
id: "4002",
|
||||
username: "owner3002",
|
||||
nickname: "Owner 3002",
|
||||
avatar: "/avatars/owner3002.png",
|
||||
role: "owner",
|
||||
createdAt,
|
||||
},
|
||||
name: "Yuki Studio",
|
||||
description: "",
|
||||
rating: "4.2",
|
||||
totalOrders: 50,
|
||||
playerCount: 1,
|
||||
commissionType: "fixed",
|
||||
commissionValue: "0",
|
||||
allowMultiShop: false,
|
||||
allowIndependentOrders: false,
|
||||
dispatchMode: "manual",
|
||||
announcements: [],
|
||||
templateConfig: { sections: [] },
|
||||
},
|
||||
{
|
||||
id: "3003",
|
||||
owner: {
|
||||
id: "4003",
|
||||
username: "owner3003",
|
||||
nickname: "Owner 3003",
|
||||
avatar: "/avatars/owner3003.png",
|
||||
role: "owner",
|
||||
createdAt,
|
||||
},
|
||||
name: "Quiet Shop",
|
||||
description: "",
|
||||
rating: "3.8",
|
||||
totalOrders: 10,
|
||||
playerCount: 2,
|
||||
commissionType: "fixed",
|
||||
commissionValue: "0",
|
||||
allowMultiShop: false,
|
||||
allowIndependentOrders: false,
|
||||
dispatchMode: "manual",
|
||||
announcements: [],
|
||||
templateConfig: { sections: [] },
|
||||
},
|
||||
]
|
||||
|
||||
const services: SearchCatalogData["services"] = players.flatMap((p) => p.services)
|
||||
|
||||
const data: SearchCatalogData = {
|
||||
players,
|
||||
shops,
|
||||
services,
|
||||
}
|
||||
|
||||
describe("searchCatalog", () => {
|
||||
describe("q matching (case-insensitive)", () => {
|
||||
it("matches player nickname", () => {
|
||||
const res = searchCatalog({ q: "winter", limit: 50 }, data)
|
||||
const playerItems = res.items.filter((i) => i.type === "player")
|
||||
expect(playerItems.some((i) => i.type === "player" && i.player.id === "1006")).toBe(true)
|
||||
})
|
||||
|
||||
it("matches player nickname case-insensitively", () => {
|
||||
const res = searchCatalog({ q: "WINTER", limit: 50 }, data)
|
||||
const playerItems = res.items.filter((i) => i.type === "player")
|
||||
expect(playerItems.some((i) => i.type === "player" && i.player.id === "1006")).toBe(true)
|
||||
})
|
||||
|
||||
it("matches shop name", () => {
|
||||
const res = searchCatalog({ q: "yuki", limit: 50 }, data)
|
||||
const shopItems = res.items.filter((i) => i.type === "shop")
|
||||
expect(shopItems.some((i) => i.type === "shop" && i.shop.id === "3002")).toBe(true)
|
||||
})
|
||||
|
||||
it("matches player game name", () => {
|
||||
const res = searchCatalog({ q: "cs2", limit: 50 }, data)
|
||||
const playerItems = res.items.filter((i) => i.type === "player")
|
||||
// 1008 has CS2 in games
|
||||
expect(playerItems.some((i) => i.type === "player" && i.player.id === "1008")).toBe(true)
|
||||
})
|
||||
|
||||
it("matches shop derived games", () => {
|
||||
const res = searchCatalog({ q: "cs2", limit: 50 }, data)
|
||||
const shopItems = res.items.filter((i) => i.type === "shop")
|
||||
expect(shopItems.some((i) => i.type === "shop" && i.shop.id === "3001")).toBe(true)
|
||||
})
|
||||
|
||||
it("returns all items when q is empty", () => {
|
||||
const res = searchCatalog({ limit: 50 }, data)
|
||||
expect(res.meta.total).toBe(players.length + shops.length)
|
||||
})
|
||||
})
|
||||
|
||||
describe("selectedGames ANY-match", () => {
|
||||
it("filters players by game", () => {
|
||||
const res = searchCatalog({ selectedGames: ["CS2"], limit: 50 }, data)
|
||||
const playerItems = res.items.filter((i) => i.type === "player")
|
||||
expect(playerItems.every((i) => i.type === "player" && i.player.games.includes("CS2"))).toBe(
|
||||
true,
|
||||
)
|
||||
expect(playerItems.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("filters shops by derived games from services", () => {
|
||||
const res = searchCatalog({ selectedGames: ["CS2"], limit: 50 }, data)
|
||||
const shopItems = res.items.filter((i) => i.type === "shop")
|
||||
expect(shopItems.some((i) => i.type === "shop" && i.shop.id === "3001")).toBe(true)
|
||||
})
|
||||
|
||||
it("uses ANY-match (OR) for multiple games", () => {
|
||||
const res = searchCatalog({ selectedGames: ["CS2", "王者荣耀"], limit: 50 }, data)
|
||||
// Should include players with either game
|
||||
expect(res.meta.total).toBeGreaterThan(0)
|
||||
const playerItems = res.items.filter((i) => i.type === "player")
|
||||
for (const item of playerItems) {
|
||||
if (item.type === "player") {
|
||||
expect(item.player.games.includes("CS2") || item.player.games.includes("王者荣耀")).toBe(
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("onlyOnline semantics", () => {
|
||||
it("filters players by status === available", () => {
|
||||
const res = searchCatalog({ onlyOnline: true, limit: 50 }, data)
|
||||
const playerItems = res.items.filter((i) => i.type === "player")
|
||||
for (const item of playerItems) {
|
||||
if (item.type === "player") {
|
||||
expect(item.player.status).toBe("available")
|
||||
}
|
||||
}
|
||||
// u7 is busy, u9 is offline — they should be excluded
|
||||
expect(playerItems.some((i) => i.type === "player" && i.player.id === "1007")).toBe(false)
|
||||
expect(playerItems.some((i) => i.type === "player" && i.player.id === "1009")).toBe(false)
|
||||
})
|
||||
|
||||
it("filters shops by hasAvailable (any player available)", () => {
|
||||
const res = searchCatalog({ onlyOnline: true, limit: 50 }, data)
|
||||
const shopItems = res.items.filter((i) => i.type === "shop")
|
||||
for (const item of shopItems) {
|
||||
if (item.type === "shop") {
|
||||
expect(item.hasAvailable).toBe(true)
|
||||
}
|
||||
}
|
||||
expect(shopItems.some((i) => i.type === "shop" && i.shop.id === "3003")).toBe(false)
|
||||
expect(shopItems.some((i) => i.type === "shop" && i.shop.id === "3001")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("price min/max parsing", () => {
|
||||
it("filters by min price", () => {
|
||||
const res = searchCatalog({ min: "25", limit: 50 }, data)
|
||||
for (const item of res.items) {
|
||||
expect(item.minPrice).toBeGreaterThanOrEqual(25)
|
||||
}
|
||||
})
|
||||
|
||||
it("filters by max price", () => {
|
||||
const res = searchCatalog({ max: "20", limit: 50 }, data)
|
||||
for (const item of res.items) {
|
||||
expect(item.minPrice).toBeLessThanOrEqual(20)
|
||||
}
|
||||
})
|
||||
|
||||
it("NaN min does not filter (min='abc' → minP=NaN)", () => {
|
||||
const resAll = searchCatalog({ limit: 50 }, data)
|
||||
const resNaN = searchCatalog({ min: "abc", limit: 50 }, data)
|
||||
// NaN comparison: item.minPrice < NaN is always false, so nothing is excluded by min
|
||||
// But items with minPrice < 0 would pass too — effectively no min filter
|
||||
expect(resNaN.meta.total).toBe(resAll.meta.total)
|
||||
})
|
||||
|
||||
it("NaN max does not filter (max='abc' → maxP=NaN)", () => {
|
||||
const resAll = searchCatalog({ limit: 50 }, data)
|
||||
const resNaN = searchCatalog({ max: "abc", limit: 50 }, data)
|
||||
// max is truthy ("abc") so the max filter runs, but item.minPrice > NaN is always false
|
||||
expect(resNaN.meta.total).toBe(resAll.meta.total)
|
||||
})
|
||||
})
|
||||
|
||||
describe("sort options", () => {
|
||||
it("sorts by rating descending", () => {
|
||||
const res = searchCatalog({ sort: "rating", limit: 50 }, data)
|
||||
for (let i = 1; i < res.items.length; i++) {
|
||||
expect(res.items[i - 1].rating).toBeGreaterThanOrEqual(res.items[i].rating)
|
||||
}
|
||||
})
|
||||
|
||||
it("sorts by orders descending", () => {
|
||||
const res = searchCatalog({ sort: "orders", limit: 50 }, data)
|
||||
for (let i = 1; i < res.items.length; i++) {
|
||||
expect(res.items[i - 1].orders).toBeGreaterThanOrEqual(res.items[i].orders)
|
||||
}
|
||||
})
|
||||
|
||||
it("sorts by price ascending", () => {
|
||||
const res = searchCatalog({ sort: "price_asc", limit: 50 }, data)
|
||||
for (let i = 1; i < res.items.length; i++) {
|
||||
expect(res.items[i - 1].minPrice).toBeLessThanOrEqual(res.items[i].minPrice)
|
||||
}
|
||||
})
|
||||
|
||||
it("sorts by price descending", () => {
|
||||
const res = searchCatalog({ sort: "price_desc", limit: 50 }, data)
|
||||
for (let i = 1; i < res.items.length; i++) {
|
||||
expect(res.items[i - 1].minPrice).toBeGreaterThanOrEqual(res.items[i].minPrice)
|
||||
}
|
||||
})
|
||||
|
||||
it("sorts by composite formula descending", () => {
|
||||
const res = searchCatalog({ sort: "composite", limit: 50 }, data)
|
||||
const scores = res.items.map((i) => i.rating * Math.log10(i.orders + 1))
|
||||
for (let i = 1; i < scores.length; i++) {
|
||||
expect(scores[i - 1]).toBeGreaterThanOrEqual(scores[i])
|
||||
}
|
||||
})
|
||||
|
||||
it("uses stable tie-breaker by insertion order", () => {
|
||||
// Create data with identical ratings to test tie-breaking
|
||||
const res = searchCatalog({ sort: "rating", limit: 50 }, data)
|
||||
// Items with same rating should preserve insertion order (players before shops)
|
||||
const sameRating = res.items.filter((i) => i.rating === res.items[0].rating)
|
||||
if (sameRating.length > 1) {
|
||||
// They should be in original insertion order
|
||||
expect(sameRating.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("pagination offset/limit", () => {
|
||||
it("returns correct meta", () => {
|
||||
const res = searchCatalog({ limit: 2, offset: 0 }, data)
|
||||
expect(res.meta.limit).toBe(2)
|
||||
expect(res.meta.offset).toBe(0)
|
||||
expect(res.items.length).toBe(2)
|
||||
expect(res.meta.total).toBe(players.length + shops.length)
|
||||
})
|
||||
|
||||
it("offset skips items", () => {
|
||||
const all = searchCatalog({ limit: 50 }, data)
|
||||
const page2 = searchCatalog({ limit: 2, offset: 2 }, data)
|
||||
expect(page2.items[0]).toEqual(all.items[2])
|
||||
expect(page2.items[1]).toEqual(all.items[3])
|
||||
})
|
||||
|
||||
it("offset beyond total returns empty items", () => {
|
||||
const res = searchCatalog({ limit: 10, offset: 100 }, data)
|
||||
expect(res.items.length).toBe(0)
|
||||
expect(res.meta.total).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user