|
| 1 | +// @vitest-environment jsdom |
| 2 | +/** |
| 3 | + * GroupSidebar.test.tsx — 组筛选侧栏 + ⓘ 弹层交互测试(v0.12.2 审查修复)。 |
| 4 | + * |
| 5 | + * @ai-context: 覆盖串组场景——切换 ⓘ 弹层目标组时表单态必须重置(key=group.id |
| 6 | + * 修复):组 A 改了判类(未确认)→ 点组 B ⓘ → 弹层显示 B 且 |
| 7 | + * 判类下拉回到 B.kind(防把 A 的选择误用到 B——路径: 改判误操作)。 |
| 8 | + * invoke 全 mock(list_note_groups/count_due_cards/list_fragments/ |
| 9 | + * get_feature_flags/week_contract_status)。 |
| 10 | + */ |
| 11 | +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 12 | +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; |
| 13 | +import type { NoteGroup } from "../types"; |
| 14 | + |
| 15 | +const { invokeMock } = vi.hoisted(() => ({ invokeMock: vi.fn() })); |
| 16 | +vi.mock("@tauri-apps/api/core", () => ({ invoke: invokeMock })); |
| 17 | + |
| 18 | +import GroupSidebar from "./GroupSidebar"; |
| 19 | + |
| 20 | +const groupA: NoteGroup = { |
| 21 | + id: 1, name: "化妆课 A", terrain: "container", kind: "course", domainTag: null, |
| 22 | + source: "route", seriesKey: null, routeReason: null, routeOverridden: 0, |
| 23 | + noteCount: 3, createdAt: 0, updatedAt: 0, |
| 24 | +}; |
| 25 | +const groupB: NoteGroup = { |
| 26 | + id: 2, name: "手账 B", terrain: "container", kind: "standalone", domainTag: null, |
| 27 | + source: "route", seriesKey: null, routeReason: null, routeOverridden: 0, |
| 28 | + noteCount: 1, createdAt: 0, updatedAt: 0, |
| 29 | +}; |
| 30 | + |
| 31 | +beforeEach(() => { |
| 32 | + invokeMock.mockReset(); |
| 33 | + invokeMock.mockImplementation(async (cmd: string) => { |
| 34 | + switch (cmd) { |
| 35 | + case "list_note_groups": return [groupA, groupB]; |
| 36 | + case "count_due_cards": return 0; |
| 37 | + case "list_fragments": return []; |
| 38 | + case "get_feature_flags": return { feedCapture: true }; |
| 39 | + case "week_contract_status": |
| 40 | + return { contract: null, weekStart: 0, actualDays: 0, actualCards: 0, minimalDayMet: false }; |
| 41 | + case "override_group_route": return true; |
| 42 | + default: |
| 43 | + throw new Error(`unexpected: ${cmd}`); |
| 44 | + } |
| 45 | + }); |
| 46 | +}); |
| 47 | + |
| 48 | +afterEach(() => cleanup()); |
| 49 | + |
| 50 | +function renderSidebar() { |
| 51 | + return render( |
| 52 | + <GroupSidebar |
| 53 | + groupFilter={null} |
| 54 | + onGroupFilterChange={vi.fn()} |
| 55 | + onChanged={vi.fn()} |
| 56 | + onOpenReview={vi.fn()} |
| 57 | + selectedNoteId={null} |
| 58 | + onOpenInbox={vi.fn()} |
| 59 | + inboxActive={false} |
| 60 | + refreshToken={0} |
| 61 | + />, |
| 62 | + ); |
| 63 | +} |
| 64 | + |
| 65 | +describe("GroupSidebar ⓘ 弹层", () => { |
| 66 | + it("组行单击仅过滤(不展开)——组行与 ⓘ 为独立动作", async () => { |
| 67 | + const onGroupFilterChange = vi.fn(); |
| 68 | + render( |
| 69 | + <GroupSidebar |
| 70 | + groupFilter={null} |
| 71 | + onGroupFilterChange={onGroupFilterChange} |
| 72 | + onChanged={vi.fn()} |
| 73 | + onOpenReview={vi.fn()} |
| 74 | + selectedNoteId={null} |
| 75 | + onOpenInbox={vi.fn()} |
| 76 | + inboxActive={false} |
| 77 | + refreshToken={0} |
| 78 | + />, |
| 79 | + ); |
| 80 | + await screen.findByTestId("group-row-1"); |
| 81 | + // 单击组行 = 仅设置过滤(无展开动作——弹层不打开) |
| 82 | + fireEvent.click(screen.getByTestId("group-row-1")); |
| 83 | + expect(onGroupFilterChange).toHaveBeenCalledWith(1); |
| 84 | + expect(screen.queryByTestId("route-popover")).toBeNull(); |
| 85 | + // ⓘ 才是弹层入口 |
| 86 | + fireEvent.click(screen.getByTestId("group-info-1")); |
| 87 | + expect(await screen.findByTestId("route-popover")).toBeTruthy(); |
| 88 | + }); |
| 89 | + |
| 90 | + it("切换组时弹层表单态重置(A 的选择不串进 B)", async () => { |
| 91 | + renderSidebar(); |
| 92 | + // 打开组 A(course)弹层,改判类为 topic(未确认) |
| 93 | + fireEvent.click(await screen.findByTestId("group-info-1")); |
| 94 | + const kindA = (await screen.findByTestId("override-kind")) as HTMLSelectElement; |
| 95 | + expect(kindA.value).toBe("course"); |
| 96 | + fireEvent.change(kindA, { target: { value: "topic" } }); |
| 97 | + |
| 98 | + // 切到组 B(standalone)——key 重建:表单态回到 B.kind |
| 99 | + fireEvent.click(screen.getByTestId("group-info-2")); |
| 100 | + const kindB = (await screen.findByTestId("override-kind")) as HTMLSelectElement; |
| 101 | + await waitFor(() => expect(kindB.value).toBe("standalone")); |
| 102 | + // 明细折叠态与结算计划同样重置 |
| 103 | + expect(screen.queryByTestId("route-details")).toBeNull(); |
| 104 | + }); |
| 105 | +}); |
0 commit comments