mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-14 08:26:46 +02:00
Settle published, successful Slack turns as Idle; resume the same conversation on an admitted message. Preserve unfinished work, delivery errors, and explicit dispositions. Verified through focused lifecycle/API/UI tests, full CI, and a real staging Slack conversation in the embedded browser. Co-Authored-By: Paperclip <noreply@paperclip.ing>
566 lines
20 KiB
TypeScript
566 lines
20 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act, type ReactNode } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
import { SidebarRecentTasks } from "./SidebarRecentTasks";
|
|
import {
|
|
getRecentTasksStorageKey,
|
|
readRecentTasks,
|
|
recordRecentTask,
|
|
pruneRecentTasks,
|
|
} from "@/lib/recent-tasks";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
|
|
const mockAuthApi = vi.hoisted(() => ({ getSession: vi.fn() }));
|
|
const mockAgentsApi = vi.hoisted(() => ({ wakeup: vi.fn() }));
|
|
const mockIssuesApi = vi.hoisted(() => ({
|
|
get: vi.fn(),
|
|
update: vi.fn(),
|
|
archiveFromInbox: vi.fn(),
|
|
getTreeControlState: vi.fn(),
|
|
createTreeHold: vi.fn(),
|
|
releaseTreeHold: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/api/agents", () => ({ agentsApi: mockAgentsApi }));
|
|
vi.mock("@/api/auth", () => ({ authApi: mockAuthApi }));
|
|
vi.mock("@/api/issues", () => ({ issuesApi: mockIssuesApi }));
|
|
vi.mock("@/lib/router", () => ({
|
|
NavLink: ({ children, to, className, ...props }: {
|
|
children: ReactNode;
|
|
to: string;
|
|
className?: string | ((state: { isActive: boolean }) => string);
|
|
}) => (
|
|
<a
|
|
href={to}
|
|
className={typeof className === "function" ? className({ isActive: false }) : className}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</a>
|
|
),
|
|
}));
|
|
vi.mock("@/context/SidebarContext", () => ({
|
|
useSidebar: () => ({
|
|
collapsed: false,
|
|
peeking: false,
|
|
isMobile: false,
|
|
setSidebarOpen: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
describe("SidebarRecentTasks", () => {
|
|
let container: HTMLDivElement;
|
|
let root: Root;
|
|
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
Object.values(mockAgentsApi).forEach((mock) => mock.mockReset());
|
|
Object.values(mockIssuesApi).forEach((mock) => mock.mockReset());
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
mockAuthApi.getSession.mockResolvedValue({
|
|
session: { id: "session-1", userId: "user-1" },
|
|
user: { id: "user-1", name: "Board", email: "board@example.test", image: null },
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
vi.clearAllMocks();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
async function render() {
|
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
await act(async () => {
|
|
root.render(
|
|
<QueryClientProvider client={queryClient}>
|
|
<TooltipProvider>
|
|
<SidebarRecentTasks companyId="company-1" liveIssueIds={new Set(["issue-1"])} />
|
|
</TooltipProvider>
|
|
</QueryClientProvider>,
|
|
);
|
|
});
|
|
await act(async () => {
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
}
|
|
});
|
|
return queryClient;
|
|
}
|
|
|
|
async function openActions(taskTitle: string) {
|
|
const actions = container.querySelector<HTMLButtonElement>(
|
|
`button[aria-label="More actions for ${taskTitle}"]`,
|
|
);
|
|
await act(async () => {
|
|
actions?.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true }));
|
|
await Promise.resolve();
|
|
});
|
|
}
|
|
|
|
function menuItem(label: string) {
|
|
return Array.from(document.body.querySelectorAll<HTMLElement>('[role="menuitem"]'))
|
|
.find((item) => item.textContent?.trim() === label);
|
|
}
|
|
|
|
function setInputValue(input: HTMLInputElement, value: string) {
|
|
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
setter?.call(input, value);
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
}
|
|
|
|
it("hides the section when there are no recent tasks", async () => {
|
|
await render();
|
|
expect(container.textContent).not.toContain("Recent Tasks");
|
|
expect(container.textContent).not.toContain("Open or create a task");
|
|
});
|
|
|
|
it("renders refreshed task text and live state without shifting for a status icon", async () => {
|
|
recordRecentTask({
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
title: "Initial title",
|
|
identifier: "PAP-1",
|
|
status: "todo",
|
|
updatedAt: new Date(1),
|
|
}, "user-1");
|
|
mockIssuesApi.get.mockResolvedValue({
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
title: "Refreshed title",
|
|
identifier: "PAP-1",
|
|
status: "in_progress",
|
|
hiddenAt: null,
|
|
updatedAt: new Date(2),
|
|
});
|
|
|
|
const queryClient = await render();
|
|
await act(async () => {
|
|
for (let index = 0; index < 5; index += 1) {
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
}
|
|
});
|
|
|
|
const link = container.querySelector('a[href="/issues/issue-1"]');
|
|
expect(mockIssuesApi.get).toHaveBeenCalledWith("issue-1");
|
|
expect(queryClient.getQueryData(["issues", "detail", "issue-1"])).toMatchObject({
|
|
title: "Refreshed title",
|
|
});
|
|
expect(link?.textContent).toContain("Refreshed title");
|
|
expect(link?.textContent).toContain("1 live");
|
|
expect(link?.querySelector('[aria-label="In Progress"]')).toBeNull();
|
|
expect(link?.querySelector('[data-slot="recent-task-icon-spacer"]')).toBeNull();
|
|
expect(link?.querySelector('[data-slot="sidebar-nav-icon"]')).toBeNull();
|
|
expect(link?.firstElementChild?.textContent).toBe("Refreshed title");
|
|
const actions = container.querySelector<HTMLButtonElement>(
|
|
'button[aria-label="More actions for Refreshed title"]',
|
|
);
|
|
expect(actions).not.toBeNull();
|
|
expect(actions?.className).toContain("opacity-0");
|
|
});
|
|
|
|
it("keeps rows stable during alternating activity and applies the final order after quiet", async () => {
|
|
const tasks = [1, 2, 3].map((id) => ({
|
|
id: `issue-${id}`,
|
|
companyId: "company-1",
|
|
title: `Task ${id}`,
|
|
identifier: `PAP-${id}`,
|
|
status: "done" as const,
|
|
hiddenAt: null,
|
|
updatedAt: new Date(id),
|
|
}));
|
|
tasks.forEach((task) => recordRecentTask(task, "user-1"));
|
|
mockIssuesApi.get.mockImplementation(async (id: string) => tasks.find((task) => task.id === id));
|
|
const queryClient = await render();
|
|
const order = () => Array.from(container.querySelectorAll("a")).map((link) => link.getAttribute("href"));
|
|
const originalOrder = ["/issues/issue-3", "/issues/issue-2", "/issues/issue-1"];
|
|
expect(order()).toEqual(originalOrder);
|
|
vi.useFakeTimers();
|
|
|
|
for (let index = 0; index < 6; index += 1) {
|
|
const task = tasks[index % 2]!;
|
|
await act(async () => {
|
|
queryClient.setQueryData(queryKeys.issues.detail(task.id), {
|
|
...task, title: `Updated ${task.id}`, updatedAt: new Date(100 + index),
|
|
});
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(500); });
|
|
expect(order()).toEqual(originalOrder);
|
|
expect(container.textContent).toContain(`Updated ${task.id}`);
|
|
}
|
|
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(499); });
|
|
expect(order()).toEqual(originalOrder);
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(1); });
|
|
expect(order()).toEqual(["/issues/issue-2", "/issues/issue-1", "/issues/issue-3"]);
|
|
expect(mockIssuesApi.get).toHaveBeenCalledTimes(3);
|
|
queryClient.clear();
|
|
});
|
|
|
|
it("adds and removes tasks immediately while an activity reorder is pending", async () => {
|
|
const task = (id: string, recordedAt: number) => ({
|
|
id, companyId: "company-1", title: id, identifier: id,
|
|
status: "todo" as const, updatedAt: new Date(recordedAt),
|
|
});
|
|
recordRecentTask(task("first", 1), "user-1");
|
|
recordRecentTask(task("second", 2), "user-1");
|
|
mockIssuesApi.get.mockImplementation(async (id: string) => task(id, 0));
|
|
const queryClient = await render();
|
|
vi.useFakeTimers();
|
|
act(() => recordRecentTask(task("first", 3), "user-1"));
|
|
expect(container.querySelector("a")?.getAttribute("href")).toBe("/issues/second");
|
|
await act(async () => {
|
|
recordRecentTask(task("new", 4), "user-1");
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
expect(container.querySelector("a")?.getAttribute("href")).toBe("/issues/new");
|
|
act(() => pruneRecentTasks(getRecentTasksStorageKey("company-1", "user-1"), "company-1", new Set(["second"])));
|
|
expect(container.querySelector('a[href="/issues/second"]')).toBeNull();
|
|
await act(async () => { await vi.advanceTimersByTimeAsync(2_000); });
|
|
expect(container.querySelector('a[href="/issues/second"]')).toBeNull();
|
|
queryClient.clear();
|
|
});
|
|
|
|
it("opens the compact task actions menu from the ellipsis button", async () => {
|
|
recordRecentTask({
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
title: "Menu task",
|
|
identifier: "PAP-1",
|
|
status: "todo",
|
|
updatedAt: new Date(1),
|
|
}, "user-1");
|
|
mockIssuesApi.get.mockResolvedValue({
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
title: "Menu task",
|
|
identifier: "PAP-1",
|
|
status: "todo",
|
|
hiddenAt: null,
|
|
updatedAt: new Date(1),
|
|
});
|
|
|
|
await render();
|
|
await openActions("Menu task");
|
|
|
|
const menu = document.body.querySelector('[data-slot="dropdown-menu-content"]');
|
|
expect(menu?.textContent).toContain("Rename");
|
|
expect(menu?.textContent).toContain("Archive");
|
|
expect(menu?.textContent).toContain("Pause/Restart");
|
|
});
|
|
|
|
it("keeps an idle Slack conversation in recent history and labels it Idle", async () => {
|
|
const issue = {
|
|
id: "issue-1", companyId: "company-1", title: "Slack conversation", identifier: "PAP-1",
|
|
status: "in_review" as const, externalConversationState: "waiting" as const,
|
|
updatedAt: new Date(1), hiddenAt: null,
|
|
};
|
|
recordRecentTask(issue, "user-1");
|
|
mockIssuesApi.get.mockResolvedValue(issue);
|
|
await render();
|
|
expect(container.textContent).toContain("Slack conversation");
|
|
expect(container.textContent).toContain("Idle");
|
|
});
|
|
|
|
it("archives a task from the inbox without hiding or removing the recent task", async () => {
|
|
const issue = {
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
title: "Archive me",
|
|
identifier: "PAP-1",
|
|
status: "todo" as const,
|
|
hiddenAt: null,
|
|
updatedAt: new Date(1),
|
|
};
|
|
recordRecentTask(issue, "user-1");
|
|
mockIssuesApi.get.mockResolvedValue(issue);
|
|
mockIssuesApi.archiveFromInbox.mockResolvedValue({
|
|
id: issue.id,
|
|
archivedAt: new Date(2),
|
|
});
|
|
|
|
await render();
|
|
await openActions("Archive me");
|
|
await act(async () => {
|
|
menuItem("Archive")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
});
|
|
|
|
expect(mockIssuesApi.archiveFromInbox).toHaveBeenCalledWith("issue-1");
|
|
expect(mockIssuesApi.update).not.toHaveBeenCalled();
|
|
expect(container.textContent).toContain("Recent Tasks");
|
|
expect(container.querySelector('a[href="/issues/issue-1"]')?.textContent).toContain(
|
|
"Archive me",
|
|
);
|
|
expect(readRecentTasks(
|
|
getRecentTasksStorageKey("company-1", "user-1"),
|
|
"company-1",
|
|
)).toHaveLength(1);
|
|
});
|
|
|
|
it("refreshes task activity after a rename", async () => {
|
|
const issue = {
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
title: "Old title",
|
|
identifier: "PAP-1",
|
|
status: "todo" as const,
|
|
hiddenAt: null,
|
|
updatedAt: new Date(1),
|
|
};
|
|
const renamedIssue = { ...issue, title: "New title", updatedAt: new Date(2) };
|
|
recordRecentTask(issue, "user-1");
|
|
mockIssuesApi.get.mockResolvedValue(issue);
|
|
mockIssuesApi.update.mockResolvedValue(renamedIssue);
|
|
|
|
const queryClient = await render();
|
|
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
|
|
await openActions("Old title");
|
|
await act(async () => {
|
|
menuItem("Rename")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const input = document.body.querySelector<HTMLInputElement>('input[aria-label="Task name"]');
|
|
expect(input).not.toBeNull();
|
|
await act(async () => {
|
|
setInputValue(input!, "New title");
|
|
await Promise.resolve();
|
|
});
|
|
await act(async () => {
|
|
input?.closest("form")?.dispatchEvent(new SubmitEvent("submit", { bubbles: true, cancelable: true }));
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
});
|
|
|
|
expect(mockIssuesApi.update).toHaveBeenCalledWith("issue-1", { title: "New title" });
|
|
expect(invalidateQueries).toHaveBeenCalledWith({
|
|
queryKey: queryKeys.issues.activity("issue-1"),
|
|
});
|
|
});
|
|
|
|
it("pauses a running task or restarts its active pause hold", async () => {
|
|
const issue = {
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
title: "Toggle work",
|
|
identifier: "PAP-1",
|
|
status: "in_progress" as const,
|
|
assigneeAgentId: "agent-1",
|
|
hiddenAt: null,
|
|
updatedAt: new Date(1),
|
|
};
|
|
recordRecentTask(issue, "user-1");
|
|
mockIssuesApi.get.mockResolvedValue(issue);
|
|
mockIssuesApi.getTreeControlState
|
|
.mockResolvedValueOnce({ activePauseHold: null })
|
|
.mockResolvedValueOnce({
|
|
activePauseHold: {
|
|
holdId: "hold-1",
|
|
rootIssueId: "issue-1",
|
|
issueId: "issue-1",
|
|
isRoot: true,
|
|
mode: "pause",
|
|
reason: null,
|
|
releasePolicy: { strategy: "manual" },
|
|
},
|
|
});
|
|
mockIssuesApi.createTreeHold.mockResolvedValue({ hold: {}, preview: {} });
|
|
mockIssuesApi.releaseTreeHold.mockResolvedValue({});
|
|
mockAgentsApi.wakeup.mockResolvedValue({ id: "run-1" });
|
|
|
|
await render();
|
|
await openActions("Toggle work");
|
|
await act(async () => {
|
|
menuItem("Pause/Restart")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
});
|
|
expect(mockIssuesApi.createTreeHold).toHaveBeenCalledWith("issue-1", {
|
|
mode: "pause",
|
|
reason: "Paused from Recent Tasks.",
|
|
releasePolicy: { strategy: "manual" },
|
|
});
|
|
|
|
await openActions("Toggle work");
|
|
await act(async () => {
|
|
menuItem("Pause/Restart")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
});
|
|
expect(mockIssuesApi.releaseTreeHold).toHaveBeenCalledWith("issue-1", "hold-1", {
|
|
reason: "Restarted from Recent Tasks.",
|
|
});
|
|
expect(mockAgentsApi.wakeup).toHaveBeenCalledWith(
|
|
"agent-1",
|
|
{
|
|
source: "assignment",
|
|
triggerDetail: "manual",
|
|
reason: "recent_task_restart",
|
|
payload: { issueId: "issue-1" },
|
|
},
|
|
"company-1",
|
|
);
|
|
});
|
|
|
|
it("retries only the wake after a remount when restart releases the hold before wakeup fails", async () => {
|
|
const issue = {
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
title: "Retry restart",
|
|
identifier: "PAP-1",
|
|
status: "in_progress" as const,
|
|
assigneeAgentId: "agent-1",
|
|
hiddenAt: null,
|
|
updatedAt: new Date(1),
|
|
};
|
|
recordRecentTask(issue, "user-1");
|
|
mockIssuesApi.get.mockResolvedValue(issue);
|
|
mockIssuesApi.getTreeControlState
|
|
.mockResolvedValueOnce({
|
|
activePauseHold: {
|
|
holdId: "hold-1",
|
|
rootIssueId: "issue-1",
|
|
issueId: "issue-1",
|
|
isRoot: true,
|
|
mode: "pause",
|
|
reason: null,
|
|
releasePolicy: { strategy: "manual" },
|
|
},
|
|
})
|
|
.mockResolvedValueOnce({ activePauseHold: null });
|
|
mockIssuesApi.releaseTreeHold.mockResolvedValue({});
|
|
mockAgentsApi.wakeup
|
|
.mockRejectedValueOnce(new Error("Wake failed"))
|
|
.mockResolvedValueOnce({ id: "run-1" });
|
|
|
|
await render();
|
|
await openActions("Retry restart");
|
|
await act(async () => {
|
|
menuItem("Pause/Restart")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
});
|
|
|
|
await act(async () => root.unmount());
|
|
root = createRoot(container);
|
|
await render();
|
|
|
|
await openActions("Retry restart");
|
|
await act(async () => {
|
|
menuItem("Pause/Restart")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
});
|
|
|
|
expect(mockIssuesApi.releaseTreeHold).toHaveBeenCalledTimes(1);
|
|
expect(mockIssuesApi.createTreeHold).not.toHaveBeenCalled();
|
|
expect(mockAgentsApi.wakeup).toHaveBeenCalledTimes(2);
|
|
expect(mockAgentsApi.wakeup).toHaveBeenLastCalledWith(
|
|
"agent-1",
|
|
{
|
|
source: "assignment",
|
|
triggerDetail: "manual",
|
|
reason: "recent_task_restart_retry",
|
|
payload: { issueId: "issue-1" },
|
|
},
|
|
"company-1",
|
|
);
|
|
});
|
|
|
|
it("synchronizes recent tasks written by another tab", async () => {
|
|
mockIssuesApi.get.mockImplementation(() => new Promise(() => {}));
|
|
await render();
|
|
const storageKey = getRecentTasksStorageKey("company-1", "user-1");
|
|
const entries = [{
|
|
id: "issue-2",
|
|
companyId: "company-1",
|
|
title: "Cross-tab task",
|
|
identifier: "PAP-2",
|
|
status: "todo" as const,
|
|
recordedAt: 2,
|
|
}];
|
|
|
|
await act(async () => {
|
|
window.localStorage.setItem(storageKey, JSON.stringify(entries));
|
|
window.dispatchEvent(new StorageEvent("storage", { key: storageKey }));
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(container.querySelector('a[href="/issues/issue-2"]')?.textContent).toContain("Cross-tab task");
|
|
});
|
|
|
|
it("preserves pending restart wake retries from before the snapshot storage migration", async () => {
|
|
const issue = {
|
|
id: "issue-1", companyId: "company-1", title: "Retry after upgrade", identifier: "PAP-1",
|
|
status: "in_progress" as const, assigneeAgentId: "agent-1", hiddenAt: null, updatedAt: new Date(1),
|
|
};
|
|
window.localStorage.setItem("paperclip.recentTasks:company-1:user-1", JSON.stringify([{ ...issue, recordedAt: 1 }]));
|
|
window.localStorage.setItem("paperclip.recentTasks:company-1:user-1:restart-wake-retry", JSON.stringify([issue.id]));
|
|
mockIssuesApi.get.mockResolvedValue(issue);
|
|
mockIssuesApi.getTreeControlState.mockResolvedValue({ activePauseHold: null });
|
|
mockAgentsApi.wakeup.mockResolvedValue({ id: "run-1" });
|
|
|
|
await render();
|
|
await openActions("Retry after upgrade");
|
|
await act(async () => {
|
|
menuItem("Pause/Restart")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
});
|
|
|
|
expect(mockIssuesApi.createTreeHold).not.toHaveBeenCalled();
|
|
expect(mockIssuesApi.releaseTreeHold).not.toHaveBeenCalled();
|
|
expect(mockAgentsApi.wakeup).toHaveBeenCalledWith("agent-1", expect.objectContaining({
|
|
reason: "recent_task_restart_retry",
|
|
}), "company-1");
|
|
expect(window.localStorage.getItem("paperclip.recentTasks:company-1:user-1:restart-wake-retry")).toBeNull();
|
|
});
|
|
|
|
it("prunes tasks that become hidden", async () => {
|
|
recordRecentTask({
|
|
id: "issue-hidden",
|
|
companyId: "company-1",
|
|
title: "Hidden task",
|
|
identifier: "PAP-3",
|
|
status: "todo",
|
|
updatedAt: new Date(3),
|
|
}, "user-1");
|
|
mockIssuesApi.get.mockResolvedValue({
|
|
id: "issue-hidden",
|
|
companyId: "company-1",
|
|
title: "Hidden task",
|
|
identifier: "PAP-3",
|
|
status: "todo",
|
|
hiddenAt: new Date(),
|
|
updatedAt: new Date(4),
|
|
});
|
|
|
|
await render();
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
});
|
|
|
|
expect(container.querySelector('a[href="/issues/issue-hidden"]')).toBeNull();
|
|
expect(readRecentTasks(
|
|
getRecentTasksStorageKey("company-1", "user-1"),
|
|
"company-1",
|
|
)).toEqual([]);
|
|
});
|
|
});
|