mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(studio): fold feedback conversations and preserve unread replies
This commit is contained in:
1 parent
9a6c6828e6
commit
ec6ffb4887
7 files changed
+200
-36
No files matched your search
@@ -188,6 +188,7 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "feedback.send", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.another", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.thread.more", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.thread.toggle", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
{ id: "feedback.reply.open", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.reply.body", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.reply.send", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
|
||||
@@ -138,6 +138,10 @@ export const EN: Record<string, string> = {
|
||||
"活动": "Activity",
|
||||
"轨迹": "Trajectory",
|
||||
"对话": "Conversation",
|
||||
"展开对话": "Expand conversation",
|
||||
"收起对话": "Collapse conversation",
|
||||
"{n} 条回复": "{n} replies",
|
||||
"{n} 条新回复": "{n} new replies",
|
||||
"事件轨迹": "Event trail",
|
||||
"运行图": "Run graph",
|
||||
"你说过的话": "What you said",
|
||||
|
||||
@@ -196,6 +196,9 @@
|
||||
}
|
||||
|
||||
.fbk-thread { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
.fbk-thread-body:not([hidden]) { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; width: 100%; }
|
||||
.fbk-thread-toggle { gap: 6px; flex-wrap: wrap; }
|
||||
.fbk-thread-toggle .studio-icon { width: 12px; height: 12px; }
|
||||
.fbk-thread ol { display: flex; flex-direction: column; gap: 8px; width: 100%; margin: 0; padding: 0; list-style: none; }
|
||||
.fbk-thread li {
|
||||
display: flex; flex-direction: column; gap: 3px; max-width: min(92%, 560px); padding: 8px 11px;
|
||||
@@ -208,7 +211,7 @@
|
||||
.fbk-msg-hd { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: var(--fs-meta); }
|
||||
.fbk-msg-hd b { color: var(--text); font-weight: var(--w-med); }
|
||||
.fbk-msg-hd time { margin-left: auto; }
|
||||
.fbk-new {
|
||||
.fbk-new, .fbk-thread-new {
|
||||
padding: 0 7px; border-radius: var(--r-pill); background: var(--accent); color: var(--accent-fg);
|
||||
font: var(--w-med) var(--fs-micro)/1.6 var(--ui);
|
||||
}
|
||||
|
||||
@@ -107,8 +107,7 @@ interface Props {
|
||||
onUnread?: (n: number) => void;
|
||||
}
|
||||
|
||||
// Opening the list is what reads the replies: each report with new ones is
|
||||
// marked seen, and its "new" marks stay until the page is closed.
|
||||
// A reply is read only in an expanded thread; its "new" mark stays for this visit.
|
||||
export function FeedbackMine({ port, onFile, onUnread }: Props) {
|
||||
const [mine, setMine] = useState<Mine | null>(null);
|
||||
const [failure, setFailure] = useState<FeedbackFailure | null>(null);
|
||||
@@ -121,12 +120,24 @@ export function FeedbackMine({ port, onFile, onUnread }: Props) {
|
||||
report.current = onUnread;
|
||||
const itemsRef = useRef<Mine["items"]>([]);
|
||||
const pending = useRef(new Set<string>());
|
||||
const tell = () => report.current?.(itemsRef.current.filter((i) => i.needsInput || pending.current.has(i.receipt)).length);
|
||||
const unfolded = (receipt: string) =>
|
||||
port.feedbackSeen(receipt, newest(itemsRef.current.find((i) => i.receipt === receipt)!)).then(() => {
|
||||
pending.current.delete(receipt);
|
||||
tell();
|
||||
}).catch(() => {});
|
||||
const views = useRef(new Map<string, boolean>());
|
||||
const tell = useCallback(() => report.current?.(itemsRef.current.filter((i) => i.needsInput || pending.current.has(i.receipt)).length), []);
|
||||
const markSeen = useCallback(async (item: FeedbackItem) => {
|
||||
const upTo = newest(item);
|
||||
await port.feedbackSeen(item.receipt, upTo);
|
||||
const current = itemsRef.current.find((i) => i.receipt === item.receipt);
|
||||
if (current && newest(current) <= upTo) pending.current.delete(item.receipt);
|
||||
tell();
|
||||
}, [port, tell]);
|
||||
const view = (receipt: string, all: boolean | null) => {
|
||||
if (all === null) {
|
||||
views.current.delete(receipt);
|
||||
return;
|
||||
}
|
||||
views.current.set(receipt, all);
|
||||
const item = itemsRef.current.find((i) => i.receipt === receipt)!;
|
||||
if (pending.current.has(receipt) && (all || !foldedUnread(item))) void markSeen(item).catch(() => {});
|
||||
};
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
@@ -141,17 +152,16 @@ export function FeedbackMine({ port, onFile, onUnread }: Props) {
|
||||
return next;
|
||||
});
|
||||
itemsRef.current = m.items;
|
||||
pending.current = new Set(m.items.filter((i) => i.unreadReplies > 0 && foldedUnread(i)).map((i) => i.receipt));
|
||||
const shown = m.items.filter((i) => i.unreadReplies > 0 && !pending.current.has(i.receipt));
|
||||
const read = await Promise.allSettled(shown.map((i) => port.feedbackSeen(i.receipt, newest(i))));
|
||||
shown.forEach((i, k) => read[k]!.status === "rejected" && pending.current.add(i.receipt));
|
||||
pending.current = new Set(m.items.filter((i) => i.unreadReplies > 0).map((i) => i.receipt));
|
||||
const shown = m.items.filter((i) => i.unreadReplies > 0 && views.current.has(i.receipt) && (views.current.get(i.receipt) || !foldedUnread(i)));
|
||||
await Promise.allSettled(shown.map(markSeen));
|
||||
tell();
|
||||
const fresher = m.items.filter((i) => i.unreadReplies > 0).length;
|
||||
if (fresher > 0) setSaid(t("有 {n} 份反馈收到了新回复。", { n: fresher }));
|
||||
})
|
||||
.catch((e) => setFailure(feedbackFailure(e)))
|
||||
.finally(() => setLoading(false));
|
||||
}, [port]);
|
||||
}, [port, markSeen, tell]);
|
||||
|
||||
useEffect(load, [load]);
|
||||
|
||||
@@ -256,7 +266,7 @@ export function FeedbackMine({ port, onFile, onUnread }: Props) {
|
||||
))}
|
||||
</ol>}
|
||||
<FeedbackReviewNote item={item} />
|
||||
<FeedbackThread item={item} fresh={fresh[item.receipt] ?? 0} onShowAll={unfolded} />
|
||||
<FeedbackThread item={item} fresh={fresh[item.receipt] ?? 0} onView={view} />
|
||||
<FeedbackReplyBox port={port} item={item} limit={replyBytes} offline={mine.offline} onSent={sent} onFile={onFile} onStale={load} />
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -38,6 +38,7 @@ describe("the thread under a report", () => {
|
||||
const port = portWith(() => mine([item({ status: "answered", replies: [reply(1, "maintainer", hostile), reply(2, "user", "thanks\nsecond line")] })]));
|
||||
render(<FeedbackMine port={port} onFile={() => {}} />);
|
||||
await screen.findByText("FB-AAAA-0001");
|
||||
await userEvent.click(screen.getByRole("button", { name: /展开对话/ }));
|
||||
const msgs = [...row("FB-AAAA-0001").querySelectorAll(".fbk-thread li")];
|
||||
expect(msgs.map((li) => li.getAttribute("data-author"))).toEqual(["maintainer", "user"]);
|
||||
expect(msgs[0]!.querySelector(".fbk-msg")!.textContent).toBe(hostile);
|
||||
@@ -55,6 +56,7 @@ describe("the thread under a report", () => {
|
||||
await screen.findByText("FB-AAAA-0001");
|
||||
expect(screen.queryByText("message 3")).toBeNull();
|
||||
expect(screen.getByText("message 4")).toBeTruthy();
|
||||
await userEvent.click(screen.getByRole("button", { name: /展开对话/ }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "显示更早的 3 条" }));
|
||||
expect(screen.getByText("message 1")).toBeTruthy();
|
||||
expect(screen.queryByRole("button", { name: /显示更早/ })).toBeNull();
|
||||
@@ -218,7 +220,7 @@ describe("who may reply", () => {
|
||||
});
|
||||
|
||||
describe("reading the replies", () => {
|
||||
it("marks each report with new replies seen, keeps its New mark for this visit, and reports what still waits", async () => {
|
||||
it("marks an opened report seen, keeps its New mark for this visit, and reports what still waits", async () => {
|
||||
const items = [
|
||||
item({ receipt: "FB-AAAA-0001", status: "answered", unreadReplies: 1, replies: [reply(1, "maintainer", "old"), reply(2, "user", "ok"), reply(3, "maintainer", "fresh")] }),
|
||||
item({ receipt: "FB-AAAA-0002", status: "needs_info", needsInput: true, replies: [reply(4, "maintainer", "q")] }),
|
||||
@@ -228,6 +230,8 @@ describe("reading the replies", () => {
|
||||
const onUnread = vi.fn();
|
||||
render(<FeedbackMine port={port} onFile={() => {}} onUnread={onUnread} />);
|
||||
await screen.findByText("FB-AAAA-0001");
|
||||
expect(port.feedbackSeen).not.toHaveBeenCalled();
|
||||
await userEvent.click(within(row("FB-AAAA-0001")).getByRole("button", { name: /展开对话/ }));
|
||||
await waitFor(() => expect(onUnread).toHaveBeenCalledWith(1));
|
||||
expect(port.feedbackSeen).toHaveBeenCalledTimes(1);
|
||||
expect(port.feedbackSeen).toHaveBeenCalledWith("FB-AAAA-0001", 3);
|
||||
@@ -244,6 +248,8 @@ describe("reading the replies", () => {
|
||||
});
|
||||
const onUnread = vi.fn();
|
||||
render(<FeedbackMine port={port} onFile={() => {}} onUnread={onUnread} />);
|
||||
await userEvent.click(await screen.findByRole("button", { name: /展开对话/ }));
|
||||
await waitFor(() => expect(port.feedbackSeen).toHaveBeenCalledWith("FB-AAAA-0001", 1));
|
||||
await waitFor(() => expect(onUnread).toHaveBeenCalledWith(1));
|
||||
});
|
||||
|
||||
@@ -280,6 +286,8 @@ describe("review fixes", () => {
|
||||
port.feedbackSeen = vi.fn(async () => { throw new Error("disk"); });
|
||||
const onUnread = vi.fn();
|
||||
render(<FeedbackMine port={port} onFile={() => {}} onUnread={onUnread} />);
|
||||
await userEvent.click(await screen.findByRole("button", { name: /展开对话/ }));
|
||||
await waitFor(() => expect(port.feedbackSeen).toHaveBeenCalledWith("FB-AAAA-0001", 1));
|
||||
await waitFor(() => expect(onUnread).toHaveBeenCalledWith(1));
|
||||
expect(onUnread).not.toHaveBeenCalledWith(2);
|
||||
});
|
||||
@@ -318,6 +326,8 @@ describe("review fixes", () => {
|
||||
await screen.findByText("FB-AAAA-0001");
|
||||
await waitFor(() => expect(onUnread).toHaveBeenCalledWith(1));
|
||||
expect(port.feedbackSeen).not.toHaveBeenCalled();
|
||||
await userEvent.click(screen.getByRole("button", { name: /展开对话/ }));
|
||||
expect(port.feedbackSeen).not.toHaveBeenCalled();
|
||||
await userEvent.click(screen.getByRole("button", { name: /显示更早/ }));
|
||||
await waitFor(() => expect(port.feedbackSeen).toHaveBeenCalledWith("FB-AAAA-0001", 6));
|
||||
await waitFor(() => expect(onUnread).toHaveBeenLastCalledWith(0));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useId, useMemo, useRef, useState } from "react";
|
||||
import { current, t } from "../i18n";
|
||||
import { FEEDBACK_CODE, FEEDBACK_REPO_ISSUES, FEEDBACK_REPLYABLE, isUnderReview, type FeedbackItem, type FeedbackReply } from "../port/feedback";
|
||||
import type { AgentPort } from "../port/port";
|
||||
@@ -27,13 +27,15 @@ function when(iso: string): string {
|
||||
interface ThreadProps {
|
||||
item: FeedbackItem;
|
||||
fresh: number;
|
||||
onShowAll?: (receipt: string) => void;
|
||||
onView?: (receipt: string, all: boolean | null) => void;
|
||||
}
|
||||
|
||||
// Replies are plain text by contract: they render as text nodes, never linked,
|
||||
// formatted or interpreted, whoever wrote them.
|
||||
export function FeedbackThread({ item, fresh, onShowAll }: ThreadProps) {
|
||||
export function FeedbackThread({ item, fresh, onView }: ThreadProps) {
|
||||
const [all, setAll] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const id = useId();
|
||||
const replies = item.replies;
|
||||
const hidden = all ? 0 : Math.max(0, replies.length - SHOWN);
|
||||
const shown = replies.slice(hidden);
|
||||
@@ -42,23 +44,35 @@ export function FeedbackThread({ item, fresh, onShowAll }: ThreadProps) {
|
||||
if (replies.length === 0) return null;
|
||||
return (
|
||||
<section className="fbk-thread" aria-label={t("对话")}>
|
||||
{hidden > 0 && (
|
||||
<button type="button" className="btn sm" data-action="feedback.thread.more" onClick={() => { setAll(true); onShowAll?.(item.receipt); }}>
|
||||
{t("显示更早的 {n} 条", { n: hidden })}
|
||||
</button>
|
||||
)}
|
||||
<ol>
|
||||
{shown.map((r) => (
|
||||
<li key={r.id} data-author={r.author} data-new={isNew(r) ? "" : undefined}>
|
||||
<div className="fbk-msg-hd">
|
||||
<b>{t(AUTHOR_LABEL[r.author])}</b>
|
||||
{isNew(r) && <span className="fbk-new">{t("新")}</span>}
|
||||
<time dateTime={r.createdAt}>{when(r.createdAt)}</time>
|
||||
</div>
|
||||
<p className="fbk-msg" dir="auto">{r.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<button type="button" className="btn sm fbk-thread-toggle" data-action="feedback.thread.toggle" data-target={item.receipt} aria-expanded={expanded} aria-controls={id} onClick={() => {
|
||||
const next = !expanded;
|
||||
setExpanded(next);
|
||||
onView?.(item.receipt, next ? all : null);
|
||||
}}>
|
||||
<StudioIcon name={expanded ? "down" : "chevron"} />
|
||||
<span>{t(expanded ? "收起对话" : "展开对话")}</span>
|
||||
<span className="fbk-meta">{t("{n} 条回复", { n: replies.length })}</span>
|
||||
{fresh > 0 && <span className="fbk-thread-new">{t("{n} 条新回复", { n: fresh })}</span>}
|
||||
</button>
|
||||
<div id={id} className="fbk-thread-body" hidden={!expanded}>
|
||||
{hidden > 0 && (
|
||||
<button type="button" className="btn sm" data-action="feedback.thread.more" onClick={() => { setAll(true); onView?.(item.receipt, true); }}>
|
||||
{t("显示更早的 {n} 条", { n: hidden })}
|
||||
</button>
|
||||
)}
|
||||
<ol>
|
||||
{shown.map((r) => (
|
||||
<li key={r.id} data-author={r.author} data-new={isNew(r) ? "" : undefined}>
|
||||
<div className="fbk-msg-hd">
|
||||
<b>{t(AUTHOR_LABEL[r.author])}</b>
|
||||
{isNew(r) && <span className="fbk-new">{t("新")}</span>}
|
||||
<time dateTime={r.createdAt}>{when(r.createdAt)}</time>
|
||||
</div>
|
||||
<p className="fbk-msg" dir="auto">{r.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { act, cleanup, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "./testkit";
|
||||
import { FeedbackMine } from "./FeedbackMine";
|
||||
import { MockPort } from "../port/mock";
|
||||
import type { FeedbackItem, FeedbackMine as Mine } from "../port/feedback";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const item = (receipt = "FB-AAAA-0001", unread = 1, count = 2): FeedbackItem => ({
|
||||
receipt, category: "bug", titleSnippet: "A report", status: "answered", needsInput: false, underReview: false,
|
||||
unreadReplies: unread, createdAt: "2026-10-01T08:00:00Z", updatedAt: "2026-10-02T08:00:00Z",
|
||||
replies: Array.from({ length: count }, (_, i) => ({ id: i + 1, author: "maintainer", body: `Reply ${i + 1}`, createdAt: "2026-10-02T08:00:00Z" })),
|
||||
});
|
||||
|
||||
const mine = (items: FeedbackItem[]): Mine => ({ items, offline: false, unread: items.filter((i) => i.unreadReplies > 0 || i.needsInput).length, hasNew: false });
|
||||
const row = (receipt = "FB-AAAA-0001") => screen.getByText(receipt).closest("li")!;
|
||||
const toggle = (receipt = "FB-AAAA-0001") => within(row(receipt)).getByRole("button", { name: /展开对话|收起对话/ });
|
||||
|
||||
function setup(read: () => Mine) {
|
||||
const port = new MockPort();
|
||||
vi.spyOn(port, "myFeedback").mockImplementation(async () => read());
|
||||
const seen = vi.spyOn(port, "feedbackSeen").mockResolvedValue(undefined);
|
||||
vi.spyOn(port, "replyFeedback");
|
||||
const onUnread = vi.fn();
|
||||
render(<FeedbackMine port={port} onFile={vi.fn()} onUnread={onUnread} />);
|
||||
return { port, seen, onUnread };
|
||||
}
|
||||
|
||||
describe("folded feedback conversations", () => {
|
||||
it("starts folded, exposes reply counts and leaves hidden replies unread until opened", async () => {
|
||||
const { seen, onUnread } = setup(() => mine([item()]));
|
||||
await screen.findByText("FB-AAAA-0001");
|
||||
expect(toggle().getAttribute("aria-expanded")).toBe("false");
|
||||
expect(toggle().textContent).toContain("2 条回复");
|
||||
expect(toggle().textContent).toContain("1 条新回复");
|
||||
expect(within(row().querySelector<HTMLElement>(".fbk-thread")!).queryByRole("list")).toBeNull();
|
||||
expect(seen).not.toHaveBeenCalled();
|
||||
expect(onUnread).toHaveBeenLastCalledWith(1);
|
||||
|
||||
await userEvent.click(toggle());
|
||||
expect(toggle().getAttribute("aria-expanded")).toBe("true");
|
||||
expect(within(row().querySelector<HTMLElement>(".fbk-thread")!).getByRole("list")).toBeTruthy();
|
||||
await waitFor(() => expect(seen).toHaveBeenCalledWith("FB-AAAA-0001", 2));
|
||||
await waitFor(() => expect(onUnread).toHaveBeenLastCalledWith(0));
|
||||
await userEvent.click(toggle());
|
||||
expect(toggle().textContent).toContain("1 条新回复");
|
||||
expect(seen).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("opens and closes one conversation from the keyboard without opening its neighbour", async () => {
|
||||
setup(() => mine([item(), item("FB-AAAA-0002", 0)]));
|
||||
await screen.findByText("FB-AAAA-0002");
|
||||
toggle().focus();
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect(toggle().getAttribute("aria-expanded")).toBe("true");
|
||||
expect(toggle("FB-AAAA-0002").getAttribute("aria-expanded")).toBe("false");
|
||||
await userEvent.keyboard(" ");
|
||||
expect(toggle().getAttribute("aria-expanded")).toBe("false");
|
||||
expect(document.activeElement).toBe(toggle());
|
||||
});
|
||||
|
||||
it("keeps newly refreshed replies unread while folded, then reads them on opening", async () => {
|
||||
let count = 2;
|
||||
const { seen, onUnread } = setup(() => mine([item("FB-AAAA-0001", count - 1, count)]));
|
||||
await screen.findByText("FB-AAAA-0001");
|
||||
count = 3;
|
||||
await userEvent.click(screen.getByRole("button", { name: "刷新列表" }));
|
||||
await waitFor(() => expect(toggle().textContent).toContain("2 条新回复"));
|
||||
expect(seen).not.toHaveBeenCalled();
|
||||
expect(onUnread).toHaveBeenLastCalledWith(1);
|
||||
await userEvent.click(toggle());
|
||||
await waitFor(() => expect(seen).toHaveBeenCalledWith("FB-AAAA-0001", 3));
|
||||
});
|
||||
|
||||
it("waits for earlier unread replies too, and preserves the full view through a refresh", async () => {
|
||||
let count = 6;
|
||||
const { seen } = setup(() => mine([item("FB-AAAA-0001", count, count)]));
|
||||
await screen.findByText("FB-AAAA-0001");
|
||||
await userEvent.click(toggle());
|
||||
expect(seen).not.toHaveBeenCalled();
|
||||
await userEvent.click(screen.getByRole("button", { name: "显示更早的 2 条" }));
|
||||
await waitFor(() => expect(seen).toHaveBeenCalledWith("FB-AAAA-0001", 6));
|
||||
count = 7;
|
||||
await userEvent.click(screen.getByRole("button", { name: "刷新列表" }));
|
||||
await waitFor(() => expect(seen).toHaveBeenCalledWith("FB-AAAA-0001", 7));
|
||||
expect(toggle().getAttribute("aria-expanded")).toBe("true");
|
||||
expect(within(row()).getAllByText(/Reply \d/)).toHaveLength(7);
|
||||
});
|
||||
|
||||
it("retains a reply draft when its conversation is folded and expanded again", async () => {
|
||||
const { port } = setup(() => mine([item()]));
|
||||
await screen.findByText("FB-AAAA-0001");
|
||||
await userEvent.click(screen.getByRole("button", { name: "回复" }));
|
||||
const box = screen.getByRole<HTMLTextAreaElement>("textbox", { name: /回复/ });
|
||||
await userEvent.type(box, "My draft");
|
||||
await userEvent.click(toggle());
|
||||
await userEvent.click(toggle());
|
||||
expect(box.value).toBe("My draft");
|
||||
expect(port.replyFeedback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not let an earlier read clear replies that arrived after the thread was folded", async () => {
|
||||
let count = 2;
|
||||
const { seen, onUnread } = setup(() => mine([item("FB-AAAA-0001", 1, count)]));
|
||||
let finish!: () => void;
|
||||
seen.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve; }));
|
||||
await screen.findByText("FB-AAAA-0001");
|
||||
await userEvent.click(toggle());
|
||||
await userEvent.click(toggle());
|
||||
count = 3;
|
||||
await userEvent.click(screen.getByRole("button", { name: "刷新列表" }));
|
||||
await waitFor(() => expect(toggle().textContent).toContain("3 条回复"));
|
||||
await act(async () => finish());
|
||||
await waitFor(() => expect(onUnread).toHaveBeenLastCalledWith(1));
|
||||
await userEvent.click(toggle());
|
||||
await waitFor(() => expect(seen).toHaveBeenCalledWith("FB-AAAA-0001", 3));
|
||||
await waitFor(() => expect(onUnread).toHaveBeenLastCalledWith(0));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user