/** * Flock 登录态 hold + 选档结账:用户旅程防护清单(QA) * 覆盖:超时提醒、必填缺失、未选档、未解锁二级、非法输入 */ import { describe, expect, it } from "vitest"; import { FLOCK_HOLD_DECISION_MS, FLOCK_HOLD_MSG, FLOCK_HOLD_TOTAL_MS, buildFlockQuoteHoldState, formatFlockHoldRemainingLabel, isFlockQuoteHoldExpired, shouldAutoDeclineFlockHold, toFlockQuoteHoldPublic, } from "@/lib/constants/flock-quote-hold"; import { validateFlockCheckoutSelection } from "@/lib/flock/flock-checkout-selection"; import { emptyFlockCheckoutDetailsDraft, flockDetailsHasErrors, validateFlockCheckoutDetailsDraft, } from "@/lib/flock/flock-checkout-details-rules"; import { flockCheckoutSchema } from "@/modules/flock/checkout-validation"; import { flockHoldContinueSchema } from "@/modules/flock/hold-validation"; /** 模拟 UI 结账前置门禁(与 widget 文案对齐) */ function gateCheckoutUi(input: { holdAccepted: boolean; selectedTier: "flock_direct" | "standard" | null; selectedFlexibility: "2_day" | "1_day" | "none" | null; detailsOk: boolean; }): string | null { if (!input.holdAccepted) { return "请先在确认窗口选择「继续填写」,再完善二级信息并下单"; } if (!input.selectedTier) { return "请先选择服务档与灵活性报价"; } if (!input.selectedFlexibility) { return "请先选择服务档与灵活性报价"; } if (!input.detailsOk) { return "请先完善左侧结账详情"; } return null; } describe("用户旅程 · 60s / 5min 超时提醒", () => { it("决策窗内:提示剩余秒数;到期 auto-decline", () => { const now = 10_000; const s = buildFlockQuoteHoldState({ quoteId: "q1", sessionId: "flock_q1", customerId: "C1", nowMs: now, }); expect(formatFlockHoldRemainingLabel(s.decision_deadline_ms, now)).toBe( "1分00秒", ); expect(shouldAutoDeclineFlockHold(s, now + FLOCK_HOLD_DECISION_MS - 1)).toBe( false, ); expect(shouldAutoDeclineFlockHold(s, now + FLOCK_HOLD_DECISION_MS)).toBe( true, ); expect(FLOCK_HOLD_MSG.decisionTimeout).toMatch(/初步报价/); expect(FLOCK_HOLD_MSG.decisionTimeoutApi).toMatch(/重新询价/); }); it("继续填写后:决策窗到期不再 auto-decline;5min 仍硬停", () => { const now = 1_000; const s = buildFlockQuoteHoldState({ quoteId: "q1", sessionId: "flock_q1", customerId: "C1", nowMs: now, }); const filling = { ...s, status: "filling" as const }; expect( shouldAutoDeclineFlockHold(filling, now + FLOCK_HOLD_DECISION_MS + 10_000), ).toBe(false); expect(toFlockQuoteHoldPublic(filling, now + 90_000).available).toBe(true); expect(isFlockQuoteHoldExpired(filling, now + FLOCK_HOLD_TOTAL_MS)).toBe( true, ); expect(FLOCK_HOLD_MSG.totalTimeoutApi).toMatch(/5 分钟/); expect(FLOCK_HOLD_MSG.totalTimeout).toMatch(/保留/); }); it("GET 侧 available:决策窗过后对 awaiting 变 false,避免继续操作假象", () => { const now = 5_000; const s = buildFlockQuoteHoldState({ quoteId: "q1", sessionId: "flock_q1", customerId: "C1", nowMs: now, }); expect(toFlockQuoteHoldPublic(s, now).available).toBe(true); expect( toFlockQuoteHoldPublic(s, now + FLOCK_HOLD_DECISION_MS).available, ).toBe(false); }); }); describe("用户旅程 · 选档 / 灵活价缺失提醒", () => { it("未选服务档", () => { expect( validateFlockCheckoutSelection({ preferredTier: "express" as "standard", preferredFlexibility: "2_day", }), ).toMatch(/FlockDirect|Standard/); }); it("未选灵活性 → 明确中文", () => { expect( validateFlockCheckoutSelection({ preferredTier: "flock_direct" }), ).toMatch(/灵活性/); expect( validateFlockCheckoutSelection({ preferredTier: "standard", preferredFlexibility: null, }), ).toMatch(/灵活性/); }); it("非法灵活性 key", () => { expect( validateFlockCheckoutSelection({ preferredTier: "standard", preferredFlexibility: "3_day" as "none", }), ).toMatch(/有效的灵活性/); }); it("完整选档通过", () => { expect( validateFlockCheckoutSelection({ preferredTier: "flock_direct", preferredFlexibility: "2_day", }), ).toBeNull(); }); it("API schema:缺 preferred_flexibility → 失败", () => { const r = flockCheckoutSchema.safeParse({ customer_id: "C1", quote_id: "q1", preferred_tier: "standard", }); expect(r.success).toBe(false); if (!r.success) { expect(r.error.issues[0]?.message).toMatch(/灵活性/); } }); }); describe("用户旅程 · 二级详情必填缺失提醒", () => { it("空表单:提货/送货公司名、地址、联系人、邮箱、电话均有错", () => { const errors = validateFlockCheckoutDetailsDraft( emptyFlockCheckoutDetailsDraft(), ); expect(flockDetailsHasErrors(errors)).toBe(true); expect(errors.pickup?.company_name).toBeTruthy(); expect(errors.pickup?.contact_name).toBeTruthy(); expect(errors.pickup?.contact_phone).toBeTruthy(); expect(errors.pickup?.contact_email).toBeTruthy(); expect(errors.delivery?.company_name).toBeTruthy(); expect(errors.delivery?.address1).toBeTruthy(); expect(errors.delivery?.city).toBeTruthy(); expect(errors.delivery?.state).toBeTruthy(); expect(errors.delivery?.zip).toBeTruthy(); expect(errors.delivery?.contact_email).toBeTruthy(); }); it("use_billing 时仍要求提货联系人;送货地址仍必填", () => { const draft = emptyFlockCheckoutDetailsDraft(); draft.use_billing_for_pickup = true; draft.pickup.contact_name = ""; draft.pickup.contact_phone = ""; draft.pickup.contact_email = ""; const errors = validateFlockCheckoutDetailsDraft(draft); expect(errors.pickup?.contact_name).toBeTruthy(); expect(errors.delivery?.address1).toBeTruthy(); }); it("邮箱 / 电话格式错误有中文提示", () => { const draft = emptyFlockCheckoutDetailsDraft(); draft.use_billing_for_pickup = true; draft.pickup = { ...draft.pickup, company_name: "A", contact_name: "Alice", contact_phone: "555-123-4567", contact_email: "not-an-email", opens_at: "9:00 AM", closes_at: "5:00 PM", }; draft.delivery = { company_name: "B", address1: "1 Main", address2: "", city: "Austin", state: "TX", zip: "78701", contact_name: "Bob", contact_phone: "123", contact_email: "bob@", opens_at: "9:00 AM", closes_at: "5:00 PM", }; const errors = validateFlockCheckoutDetailsDraft(draft); expect(errors.pickup?.contact_email).toBeTruthy(); expect(errors.pickup?.contact_phone).toBeTruthy(); expect(errors.delivery?.contact_phone).toBeTruthy(); expect(errors.delivery?.contact_email).toBeTruthy(); }); it("widget 表单失败时应展示统一汇总文案(契约)", () => { // 与 flock-logged-in-details-form validateAndGetDetails 一致 expect("请先修正结账详情中的必填项与格式错误").toMatch(/必填|格式/); }); }); describe("用户旅程 · 结账门禁顺序(预防跳步)", () => { it("未点继续填写 → 拦截", () => { expect( gateCheckoutUi({ holdAccepted: false, selectedTier: "flock_direct", selectedFlexibility: "2_day", detailsOk: true, }), ).toMatch(/继续填写/); }); it("未选档 / 未选灵活 → 拦截", () => { expect( gateCheckoutUi({ holdAccepted: true, selectedTier: null, selectedFlexibility: null, detailsOk: true, }), ).toMatch(/灵活性|服务档/); }); it("详情未过校验 → 拦截", () => { expect( gateCheckoutUi({ holdAccepted: true, selectedTier: "standard", selectedFlexibility: "none", detailsOk: false, }), ).toMatch(/结账详情/); }); it("全部就绪 → 放行", () => { expect( gateCheckoutUi({ holdAccepted: true, selectedTier: "flock_direct", selectedFlexibility: "1_day", detailsOk: true, }), ).toBeNull(); }); }); describe("用户旅程 · hold API 入参防护", () => { it("continue schema:缺 quote_session_id", () => { const r = flockHoldContinueSchema.safeParse({ customer_id: "C1", quote_id: "q1", }); expect(r.success).toBe(false); }); it("continue schema:完整通过", () => { const r = flockHoldContinueSchema.safeParse({ customer_id: "C1", quote_id: "q1", quote_session_id: "flock_q1", }); expect(r.success).toBe(true); }); });