修复 TypeScript 基线并补齐测试类型

This commit is contained in:
2026-03-03 12:21:02 +08:00
parent 673dc1177e
commit 52b87abee3
9 changed files with 36 additions and 21 deletions
+8 -2
View File
@@ -133,9 +133,15 @@
- 先请求成功再更新 `planAccepted/activeContract` - 先请求成功再更新 `planAccepted/activeContract`
- 失败回滚并展示错误信息。 - 失败回滚并展示错误信息。
### P2-3 TypeScript 基线不通过(测试代码类型漂移) ### P2-3 TypeScript 基线不通过(测试代码类型漂移)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- 修复 `createRequest``NextRequest` 构造参数类型不兼容问题;
- 修复盲盒测试中的 Prisma 事务 mock 签名、SSE 计划 mock 返回结构;
- 补齐路由测试上下文参数与测试数据类型字段(`reason``creatorId``isLoading/error``matchType`);
- `npx tsc --noEmit` 已恢复通过。
- 证据: - 证据:
- `npx tsc --noEmit` 当前报多个错误(测试辅助类型、mock 签名、类型定义漂移等) - 修复后执行 `npx tsc --noEmit` 已无报错
- 影响: - 影响:
- 类型系统对回归的兜底失效;重构时风险上升。 - 类型系统对回归的兜底失效;重构时风险上升。
- 建议: - 建议:
+4 -2
View File
@@ -9,10 +9,12 @@ export function createRequest(
} = {}, } = {},
): NextRequest { ): NextRequest {
const { method = "GET", body, headers = {} } = options; const { method = "GET", body, headers = {} } = options;
const init: RequestInit = { method, headers }; type NextRequestInit = ConstructorParameters<typeof NextRequest>[1];
const requestHeaders: Record<string, string> = { ...headers };
const init: NextRequestInit = { method, headers: requestHeaders };
if (body !== undefined) { if (body !== undefined) {
init.body = JSON.stringify(body); init.body = JSON.stringify(body);
(init.headers as Record<string, string>)["content-type"] = "application/json"; requestHeaders["content-type"] = "application/json";
} }
return new NextRequest(new URL(url, "http://localhost:3721"), init); return new NextRequest(new URL(url, "http://localhost:3721"), init);
} }
+9 -9
View File
@@ -20,7 +20,7 @@ beforeEach(() => {
describe("POST /api/blindbox/draw", () => { describe("POST /api/blindbox/draw", () => {
it("draws a random idea", async () => { it("draws a random idea", async () => {
prismaMock.$transaction.mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) => { prismaMock.$transaction.mockImplementation((async (fn: unknown) => {
const tx = { const tx = {
blindBoxIdea: { blindBoxIdea: {
findMany: vi.fn().mockResolvedValue([{ id: "idea-1" }]), findMany: vi.fn().mockResolvedValue([{ id: "idea-1" }]),
@@ -34,8 +34,8 @@ describe("POST /api/blindbox/draw", () => {
}), }),
}, },
}; };
return fn(tx); return (fn as (txArg: typeof tx) => Promise<unknown>)(tx);
}); }) as never);
const req = createRequest("/api/blindbox/draw", { const req = createRequest("/api/blindbox/draw", {
method: "POST", method: "POST",
@@ -52,7 +52,7 @@ describe("POST /api/blindbox/draw", () => {
}); });
it("returns 404 when pool is empty", async () => { it("returns 404 when pool is empty", async () => {
prismaMock.$transaction.mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) => { prismaMock.$transaction.mockImplementation((async (fn: unknown) => {
const tx = { const tx = {
blindBoxIdea: { blindBoxIdea: {
findMany: vi.fn().mockResolvedValue([]), findMany: vi.fn().mockResolvedValue([]),
@@ -60,8 +60,8 @@ describe("POST /api/blindbox/draw", () => {
findUnique: vi.fn(), findUnique: vi.fn(),
}, },
}; };
return fn(tx); return (fn as (txArg: typeof tx) => Promise<unknown>)(tx);
}); }) as never);
const req = createRequest("/api/blindbox/draw", { const req = createRequest("/api/blindbox/draw", {
method: "POST", method: "POST",
@@ -72,7 +72,7 @@ describe("POST /api/blindbox/draw", () => {
}); });
it("returns 409 on race condition (count=0)", async () => { it("returns 409 on race condition (count=0)", async () => {
prismaMock.$transaction.mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) => { prismaMock.$transaction.mockImplementation((async (fn: unknown) => {
const tx = { const tx = {
blindBoxIdea: { blindBoxIdea: {
findMany: vi.fn().mockResolvedValue([{ id: "idea-1" }]), findMany: vi.fn().mockResolvedValue([{ id: "idea-1" }]),
@@ -80,8 +80,8 @@ describe("POST /api/blindbox/draw", () => {
findUnique: vi.fn(), findUnique: vi.fn(),
}, },
}; };
return fn(tx); return (fn as (txArg: typeof tx) => Promise<unknown>)(tx);
}); }) as never);
const req = createRequest("/api/blindbox/draw", { const req = createRequest("/api/blindbox/draw", {
method: "POST", method: "POST",
@@ -45,8 +45,8 @@ describe("POST /api/blindbox/plan/stream", () => {
onProgress?.("正在生成行程..."); onProgress?.("正在生成行程...");
return { return {
id: "plan-1", id: "plan-1",
days: [{ date: "周六", items: [] }], days: [{ date: "周六", items: [], summary: "轻松逛吃" }],
createdAt: new Date(), createdAt: "2025-03-01T09:00:00.000Z",
}; };
}); });
@@ -22,6 +22,7 @@ import { getAuthUserId } from "@/lib/auth";
const mockSuggestAlternativeItems = vi.mocked(suggestAlternativeItems); const mockSuggestAlternativeItems = vi.mocked(suggestAlternativeItems);
const mockSearchPois = vi.mocked(searchPois); const mockSearchPois = vi.mocked(searchPois);
const mockGetAuthUserId = vi.mocked(getAuthUserId); const mockGetAuthUserId = vi.mocked(getAuthUserId);
const mockCtx = { params: Promise.resolve({}) };
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@@ -35,7 +36,7 @@ describe("POST /api/blindbox/plan/suggest-item", () => {
method: "POST", method: "POST",
body: { activity: "看展" }, body: { activity: "看展" },
}); });
const res = await POST(req); const res = await POST(req, mockCtx);
expect(res.status).toBe(401); expect(res.status).toBe(401);
}); });
@@ -65,7 +66,7 @@ describe("POST /api/blindbox/plan/suggest-item", () => {
location: "121.47,31.23", location: "121.47,31.23",
}, },
}); });
const res = await POST(req); const res = await POST(req, mockCtx);
const { status, data } = await parseJsonResponse(res); const { status, data } = await parseJsonResponse(res);
expect(status).toBe(200); expect(status).toBe(200);
+2 -2
View File
@@ -36,9 +36,9 @@ describe("GET /api/blindbox/rooms", () => {
}, },
] as never); ] as never);
prismaMock.blindBoxIdea.groupBy.mockResolvedValue([ prismaMock.blindBoxIdea.groupBy = vi.fn().mockResolvedValue([
{ roomId: "bb-room-1", _count: 3 }, { roomId: "bb-room-1", _count: 3 },
] as never); ]) as never;
const req = createRequest("/api/blindbox/rooms"); const req = createRequest("/api/blindbox/rooms");
const res = await GET(req, mockCtx); const res = await GET(req, mockCtx);
+5 -1
View File
@@ -46,6 +46,8 @@ vi.mock("@/hooks/useRoomPolling", () => ({
users: ["user-1", "user-2"], users: ["user-1", "user-2"],
userProfiles: {}, userProfiles: {},
scene: "eat", scene: "eat",
isLoading: false,
error: undefined,
}), }),
})); }));
@@ -114,11 +116,13 @@ describe("RoomPage", () => {
restaurants: [], restaurants: [],
notFound: true, notFound: true,
mutate: vi.fn(), mutate: vi.fn(),
creatorId: null, creatorId: "",
locked: false, locked: false,
users: [], users: [],
userProfiles: {}, userProfiles: {},
scene: "eat", scene: "eat",
isLoading: false,
error: undefined,
}); });
renderPage(); renderPage();
+2
View File
@@ -36,6 +36,7 @@ const mockDays: WeekendPlanData[] = [
duration: 90, duration: 90,
lat: 39.9, lat: 39.9,
lng: 116.4, lng: 116.4,
reason: "补充体力",
}, },
], ],
}, },
@@ -51,6 +52,7 @@ const mockDays: WeekendPlanData[] = [
duration: 180, duration: 180,
lat: 39.9, lat: 39.9,
lng: 116.4, lng: 116.4,
reason: "文化体验",
}, },
], ],
}, },
+1 -1
View File
@@ -41,7 +41,7 @@ const defaultProps = {
userId: "user-1", userId: "user-1",
initialIndex: 0, initialIndex: 0,
matchedRestaurantId: null, matchedRestaurantId: null,
matchType: null as const, matchType: null,
matchLikes: 0, matchLikes: 0,
runnerUps: [], runnerUps: [],
likeCounts: {}, likeCounts: {},