640 lines
19 KiB
TypeScript
640 lines
19 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
import type {
|
|
Approval,
|
|
DashboardSummary,
|
|
ExecutionWorkspace,
|
|
HeartbeatRun,
|
|
Issue,
|
|
JoinRequest,
|
|
ProjectWorkspace,
|
|
} from "@paperclipai/shared";
|
|
import {
|
|
DEFAULT_INBOX_ISSUE_COLUMNS,
|
|
computeInboxBadgeData,
|
|
getAvailableInboxIssueColumns,
|
|
getApprovalsForTab,
|
|
getInboxWorkItems,
|
|
getInboxKeyboardSelectionIndex,
|
|
getRecentTouchedIssues,
|
|
getUnreadTouchedIssues,
|
|
isMineInboxTab,
|
|
loadInboxIssueColumns,
|
|
loadLastInboxTab,
|
|
normalizeInboxIssueColumns,
|
|
RECENT_ISSUES_LIMIT,
|
|
resolveIssueWorkspaceName,
|
|
resolveInboxSelectionIndex,
|
|
saveInboxIssueColumns,
|
|
saveLastInboxTab,
|
|
shouldShowInboxSection,
|
|
} from "./inbox";
|
|
|
|
const storage = new Map<string, string>();
|
|
|
|
Object.defineProperty(globalThis, "localStorage", {
|
|
value: {
|
|
getItem: (key: string) => storage.get(key) ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
storage.set(key, value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
storage.delete(key);
|
|
},
|
|
clear: () => {
|
|
storage.clear();
|
|
},
|
|
},
|
|
configurable: true,
|
|
});
|
|
|
|
function makeApproval(status: Approval["status"]): Approval {
|
|
return {
|
|
id: `approval-${status}`,
|
|
companyId: "company-1",
|
|
type: "hire_agent",
|
|
requestedByAgentId: null,
|
|
requestedByUserId: null,
|
|
status,
|
|
payload: {},
|
|
decisionNote: null,
|
|
decidedByUserId: null,
|
|
decidedAt: null,
|
|
createdAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
};
|
|
}
|
|
|
|
function makeApprovalWithTimestamps(
|
|
id: string,
|
|
status: Approval["status"],
|
|
updatedAt: string,
|
|
): Approval {
|
|
return {
|
|
...makeApproval(status),
|
|
id,
|
|
createdAt: new Date(updatedAt),
|
|
updatedAt: new Date(updatedAt),
|
|
};
|
|
}
|
|
|
|
function makeJoinRequest(id: string): JoinRequest {
|
|
return {
|
|
id,
|
|
inviteId: "invite-1",
|
|
companyId: "company-1",
|
|
requestType: "human",
|
|
status: "pending_approval",
|
|
requestEmailSnapshot: null,
|
|
requestIp: "127.0.0.1",
|
|
requestingUserId: null,
|
|
agentName: null,
|
|
adapterType: null,
|
|
capabilities: null,
|
|
agentDefaultsPayload: null,
|
|
claimSecretExpiresAt: null,
|
|
claimSecretConsumedAt: null,
|
|
createdAgentId: null,
|
|
approvedByUserId: null,
|
|
approvedAt: null,
|
|
rejectedByUserId: null,
|
|
rejectedAt: null,
|
|
createdAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
};
|
|
}
|
|
|
|
function makeRun(id: string, status: HeartbeatRun["status"], createdAt: string, agentId = "agent-1"): HeartbeatRun {
|
|
return {
|
|
id,
|
|
companyId: "company-1",
|
|
agentId,
|
|
invocationSource: "assignment",
|
|
triggerDetail: null,
|
|
status,
|
|
error: null,
|
|
wakeupRequestId: null,
|
|
exitCode: null,
|
|
signal: null,
|
|
usageJson: null,
|
|
resultJson: null,
|
|
sessionIdBefore: null,
|
|
sessionIdAfter: null,
|
|
logStore: null,
|
|
logRef: null,
|
|
logBytes: null,
|
|
logSha256: null,
|
|
logCompressed: false,
|
|
errorCode: null,
|
|
externalRunId: null,
|
|
processPid: null,
|
|
processStartedAt: null,
|
|
retryOfRunId: null,
|
|
processLossRetryCount: 0,
|
|
stdoutExcerpt: null,
|
|
stderrExcerpt: null,
|
|
contextSnapshot: null,
|
|
startedAt: new Date(createdAt),
|
|
finishedAt: null,
|
|
createdAt: new Date(createdAt),
|
|
updatedAt: new Date(createdAt),
|
|
};
|
|
}
|
|
|
|
function makeIssue(id: string, isUnreadForMe: boolean): Issue {
|
|
return {
|
|
id,
|
|
companyId: "company-1",
|
|
projectId: null,
|
|
projectWorkspaceId: null,
|
|
goalId: null,
|
|
parentId: null,
|
|
title: `Issue ${id}`,
|
|
description: null,
|
|
status: "todo",
|
|
priority: "medium",
|
|
assigneeAgentId: null,
|
|
assigneeUserId: null,
|
|
createdByAgentId: null,
|
|
createdByUserId: null,
|
|
issueNumber: 1,
|
|
identifier: `PAP-${id}`,
|
|
requestDepth: 0,
|
|
billingCode: null,
|
|
assigneeAdapterOverrides: null,
|
|
executionWorkspaceId: null,
|
|
executionWorkspacePreference: null,
|
|
executionWorkspaceSettings: null,
|
|
checkoutRunId: null,
|
|
executionRunId: null,
|
|
executionAgentNameKey: null,
|
|
executionLockedAt: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
cancelledAt: null,
|
|
hiddenAt: null,
|
|
createdAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
labels: [],
|
|
labelIds: [],
|
|
myLastTouchAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
lastExternalCommentAt: new Date("2026-03-11T01:00:00.000Z"),
|
|
lastActivityAt: new Date("2026-03-11T01:00:00.000Z"),
|
|
isUnreadForMe,
|
|
};
|
|
}
|
|
|
|
function makeProjectWorkspace(overrides: Partial<ProjectWorkspace> = {}): ProjectWorkspace {
|
|
return {
|
|
id: "project-workspace-1",
|
|
companyId: "company-1",
|
|
projectId: "project-1",
|
|
name: "Primary workspace",
|
|
sourceType: "local_path",
|
|
cwd: "/tmp/project",
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
defaultRef: null,
|
|
visibility: "default",
|
|
setupCommand: null,
|
|
cleanupCommand: null,
|
|
remoteProvider: null,
|
|
remoteWorkspaceRef: null,
|
|
sharedWorkspaceKey: null,
|
|
metadata: null,
|
|
runtimeConfig: null,
|
|
isPrimary: true,
|
|
createdAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeExecutionWorkspace(overrides: Partial<ExecutionWorkspace> = {}): ExecutionWorkspace {
|
|
return {
|
|
id: "execution-workspace-1",
|
|
companyId: "company-1",
|
|
projectId: "project-1",
|
|
projectWorkspaceId: "project-workspace-1",
|
|
sourceIssueId: "issue-1",
|
|
mode: "isolated_workspace",
|
|
strategyType: "git_worktree",
|
|
name: "PAP-1 branch",
|
|
status: "active",
|
|
cwd: "/tmp/project/worktree",
|
|
repoUrl: null,
|
|
baseRef: null,
|
|
branchName: "pap-1",
|
|
providerType: "git_worktree",
|
|
providerRef: null,
|
|
derivedFromExecutionWorkspaceId: null,
|
|
lastUsedAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
openedAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
closedAt: null,
|
|
cleanupEligibleAt: null,
|
|
cleanupReason: null,
|
|
config: null,
|
|
metadata: null,
|
|
createdAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const dashboard: DashboardSummary = {
|
|
companyId: "company-1",
|
|
agents: {
|
|
active: 1,
|
|
running: 0,
|
|
paused: 0,
|
|
error: 1,
|
|
},
|
|
tasks: {
|
|
open: 1,
|
|
inProgress: 0,
|
|
blocked: 0,
|
|
done: 0,
|
|
},
|
|
costs: {
|
|
monthSpendCents: 900,
|
|
monthBudgetCents: 1000,
|
|
monthUtilizationPercent: 90,
|
|
},
|
|
pendingApprovals: 1,
|
|
budgets: {
|
|
activeIncidents: 0,
|
|
pendingApprovals: 0,
|
|
pausedAgents: 0,
|
|
pausedProjects: 0,
|
|
},
|
|
};
|
|
|
|
describe("inbox helpers", () => {
|
|
beforeEach(() => {
|
|
storage.clear();
|
|
});
|
|
|
|
it("counts the same inbox sources the badge uses", () => {
|
|
const result = computeInboxBadgeData({
|
|
approvals: [makeApproval("pending"), makeApproval("approved")],
|
|
joinRequests: [makeJoinRequest("join-1")],
|
|
dashboard,
|
|
heartbeatRuns: [
|
|
makeRun("run-old", "failed", "2026-03-11T00:00:00.000Z"),
|
|
makeRun("run-latest", "timed_out", "2026-03-11T01:00:00.000Z"),
|
|
makeRun("run-other-agent", "failed", "2026-03-11T02:00:00.000Z", "agent-2"),
|
|
],
|
|
mineIssues: [makeIssue("1", true)],
|
|
dismissed: new Set<string>(),
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
inbox: 6,
|
|
approvals: 1,
|
|
failedRuns: 2,
|
|
joinRequests: 1,
|
|
mineIssues: 1,
|
|
alerts: 1,
|
|
});
|
|
});
|
|
|
|
it("drops dismissed runs and alerts from the computed badge", () => {
|
|
const result = computeInboxBadgeData({
|
|
approvals: [],
|
|
joinRequests: [],
|
|
dashboard,
|
|
heartbeatRuns: [makeRun("run-1", "failed", "2026-03-11T00:00:00.000Z")],
|
|
mineIssues: [],
|
|
dismissed: new Set<string>(["run:run-1", "alert:budget", "alert:agent-errors"]),
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
inbox: 0,
|
|
approvals: 0,
|
|
failedRuns: 0,
|
|
joinRequests: 0,
|
|
mineIssues: 0,
|
|
alerts: 0,
|
|
});
|
|
});
|
|
|
|
it("keeps read issues in the touched list but excludes them from unread counts", () => {
|
|
const issues = [makeIssue("1", true), makeIssue("2", false)];
|
|
|
|
expect(getUnreadTouchedIssues(issues).map((issue) => issue.id)).toEqual(["1"]);
|
|
expect(issues).toHaveLength(2);
|
|
});
|
|
|
|
it("shows recent approvals in updated order and unread approvals as actionable only", () => {
|
|
const approvals = [
|
|
makeApprovalWithTimestamps("approval-approved", "approved", "2026-03-11T02:00:00.000Z"),
|
|
makeApprovalWithTimestamps("approval-pending", "pending", "2026-03-11T01:00:00.000Z"),
|
|
makeApprovalWithTimestamps(
|
|
"approval-revision",
|
|
"revision_requested",
|
|
"2026-03-11T03:00:00.000Z",
|
|
),
|
|
];
|
|
|
|
expect(getApprovalsForTab(approvals, "mine", "all").map((approval) => approval.id)).toEqual([
|
|
"approval-revision",
|
|
"approval-approved",
|
|
"approval-pending",
|
|
]);
|
|
expect(getApprovalsForTab(approvals, "recent", "all").map((approval) => approval.id)).toEqual([
|
|
"approval-revision",
|
|
"approval-approved",
|
|
"approval-pending",
|
|
]);
|
|
expect(getApprovalsForTab(approvals, "unread", "all").map((approval) => approval.id)).toEqual([
|
|
"approval-revision",
|
|
"approval-pending",
|
|
]);
|
|
expect(getApprovalsForTab(approvals, "all", "resolved").map((approval) => approval.id)).toEqual([
|
|
"approval-approved",
|
|
]);
|
|
});
|
|
|
|
it("mixes approvals into the inbox feed by most recent activity", () => {
|
|
const newerIssue = makeIssue("1", true);
|
|
newerIssue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
|
|
|
|
const olderIssue = makeIssue("2", false);
|
|
olderIssue.lastActivityAt = new Date("2026-03-11T02:00:00.000Z");
|
|
|
|
const approval = makeApprovalWithTimestamps(
|
|
"approval-between",
|
|
"pending",
|
|
"2026-03-11T03:00:00.000Z",
|
|
);
|
|
|
|
expect(
|
|
getInboxWorkItems({
|
|
issues: [olderIssue, newerIssue],
|
|
approvals: [approval],
|
|
}).map((item) => {
|
|
if (item.kind === "issue") return `issue:${item.issue.id}`;
|
|
if (item.kind === "approval") return `approval:${item.approval.id}`;
|
|
if (item.kind === "join_request") return `join:${item.joinRequest.id}`;
|
|
return `run:${item.run.id}`;
|
|
}),
|
|
).toEqual([
|
|
"issue:1",
|
|
"approval:approval-between",
|
|
"issue:2",
|
|
]);
|
|
});
|
|
|
|
it("prefers canonical lastActivityAt over comment-only timestamps", () => {
|
|
const activityIssue = makeIssue("1", true);
|
|
activityIssue.lastExternalCommentAt = new Date("2026-03-11T01:00:00.000Z");
|
|
activityIssue.lastActivityAt = new Date("2026-03-11T05:00:00.000Z");
|
|
|
|
const commentIssue = makeIssue("2", true);
|
|
commentIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
|
|
commentIssue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
|
|
|
|
expect(getRecentTouchedIssues([commentIssue, activityIssue]).map((issue) => issue.id)).toEqual(["1", "2"]);
|
|
});
|
|
|
|
it("mixes join requests into the inbox feed by most recent activity", () => {
|
|
const issue = makeIssue("1", true);
|
|
issue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z");
|
|
|
|
const joinRequest = makeJoinRequest("join-1");
|
|
joinRequest.createdAt = new Date("2026-03-11T03:00:00.000Z");
|
|
|
|
const approval = makeApprovalWithTimestamps(
|
|
"approval-oldest",
|
|
"pending",
|
|
"2026-03-11T02:00:00.000Z",
|
|
);
|
|
|
|
expect(
|
|
getInboxWorkItems({
|
|
issues: [issue],
|
|
approvals: [approval],
|
|
joinRequests: [joinRequest],
|
|
}).map((item) => {
|
|
if (item.kind === "issue") return `issue:${item.issue.id}`;
|
|
if (item.kind === "approval") return `approval:${item.approval.id}`;
|
|
if (item.kind === "join_request") return `join:${item.joinRequest.id}`;
|
|
return `run:${item.run.id}`;
|
|
}),
|
|
).toEqual([
|
|
"issue:1",
|
|
"join:join-1",
|
|
"approval:approval-oldest",
|
|
]);
|
|
});
|
|
|
|
it("sorts self-touched issues without external comments by updatedAt", () => {
|
|
const recentSelfTouched = makeIssue("recent", false);
|
|
recentSelfTouched.lastExternalCommentAt = null as unknown as Date;
|
|
recentSelfTouched.updatedAt = new Date("2026-03-11T05:00:00.000Z");
|
|
recentSelfTouched.myLastTouchAt = new Date("2026-03-11T05:00:00.000Z");
|
|
|
|
const olderCommented = makeIssue("older", false);
|
|
olderCommented.lastExternalCommentAt = new Date("2026-03-11T03:00:00.000Z");
|
|
|
|
const items = getInboxWorkItems({
|
|
issues: [olderCommented, recentSelfTouched],
|
|
approvals: [],
|
|
});
|
|
|
|
expect(items.map((i) => (i.kind === "issue" ? i.issue.id : ""))).toEqual([
|
|
"recent",
|
|
"older",
|
|
]);
|
|
});
|
|
|
|
it("can include sections on recent without forcing them to be unread", () => {
|
|
expect(
|
|
shouldShowInboxSection({
|
|
tab: "mine",
|
|
hasItems: true,
|
|
showOnMine: true,
|
|
showOnRecent: false,
|
|
showOnUnread: false,
|
|
showOnAll: false,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldShowInboxSection({
|
|
tab: "recent",
|
|
hasItems: true,
|
|
showOnMine: false,
|
|
showOnRecent: true,
|
|
showOnUnread: false,
|
|
showOnAll: false,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
shouldShowInboxSection({
|
|
tab: "unread",
|
|
hasItems: true,
|
|
showOnMine: true,
|
|
showOnRecent: true,
|
|
showOnUnread: false,
|
|
showOnAll: false,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("limits recent touched issues before unread badge counting", () => {
|
|
const issues = Array.from({ length: RECENT_ISSUES_LIMIT + 5 }, (_, index) => {
|
|
const issue = makeIssue(String(index + 1), index < 3);
|
|
issue.lastActivityAt = new Date(Date.UTC(2026, 2, 31, 0, 0, 0, 0) - index * 60_000);
|
|
return issue;
|
|
});
|
|
|
|
const recentIssues = getRecentTouchedIssues(issues);
|
|
|
|
expect(recentIssues).toHaveLength(RECENT_ISSUES_LIMIT);
|
|
expect(getUnreadTouchedIssues(recentIssues).map((issue) => issue.id)).toEqual(["1", "2", "3"]);
|
|
});
|
|
|
|
it("defaults the remembered inbox tab to mine and persists all", () => {
|
|
localStorage.clear();
|
|
expect(loadLastInboxTab()).toBe("mine");
|
|
|
|
saveLastInboxTab("all");
|
|
expect(loadLastInboxTab()).toBe("all");
|
|
});
|
|
|
|
it("defaults issue columns to the current inbox layout", () => {
|
|
expect(loadInboxIssueColumns()).toEqual(DEFAULT_INBOX_ISSUE_COLUMNS);
|
|
});
|
|
|
|
it("normalizes saved issue columns to valid values in canonical order", () => {
|
|
saveInboxIssueColumns(["labels", "updated", "status", "workspace", "labels", "assignee"]);
|
|
|
|
expect(loadInboxIssueColumns()).toEqual(["status", "assignee", "workspace", "labels", "updated"]);
|
|
expect(normalizeInboxIssueColumns(["project", "workspace", "wat", "id"])).toEqual(["id", "project", "workspace"]);
|
|
});
|
|
|
|
it("hides the workspace column option unless isolated workspaces are enabled", () => {
|
|
expect(getAvailableInboxIssueColumns(false)).toEqual(["status", "id", "assignee", "project", "labels", "updated"]);
|
|
expect(getAvailableInboxIssueColumns(true)).toEqual([
|
|
"status",
|
|
"id",
|
|
"assignee",
|
|
"project",
|
|
"workspace",
|
|
"labels",
|
|
"updated",
|
|
]);
|
|
});
|
|
|
|
it("allows hiding every optional issue column down to the title-only view", () => {
|
|
saveInboxIssueColumns([]);
|
|
expect(loadInboxIssueColumns()).toEqual([]);
|
|
});
|
|
|
|
it("shows explicit workspace names but leaves the default workspace blank", () => {
|
|
const issue = makeIssue("1", true);
|
|
issue.projectId = "project-1";
|
|
issue.projectWorkspaceId = "project-workspace-1";
|
|
issue.executionWorkspaceId = "execution-workspace-1";
|
|
|
|
const executionWorkspace = makeExecutionWorkspace();
|
|
const defaultWorkspace = makeProjectWorkspace();
|
|
const secondaryWorkspace = makeProjectWorkspace({
|
|
id: "project-workspace-2",
|
|
name: "Secondary workspace",
|
|
isPrimary: false,
|
|
});
|
|
|
|
expect(
|
|
resolveIssueWorkspaceName(issue, {
|
|
executionWorkspaceById: new Map([[executionWorkspace.id, executionWorkspace]]),
|
|
projectWorkspaceById: new Map([
|
|
[defaultWorkspace.id, defaultWorkspace],
|
|
[secondaryWorkspace.id, secondaryWorkspace],
|
|
]),
|
|
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
|
}),
|
|
).toBe("PAP-1 branch");
|
|
|
|
issue.executionWorkspaceId = null;
|
|
expect(
|
|
resolveIssueWorkspaceName(issue, {
|
|
projectWorkspaceById: new Map([
|
|
[defaultWorkspace.id, defaultWorkspace],
|
|
[secondaryWorkspace.id, secondaryWorkspace],
|
|
]),
|
|
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
|
}),
|
|
).toBeNull();
|
|
|
|
issue.projectWorkspaceId = secondaryWorkspace.id;
|
|
expect(
|
|
resolveIssueWorkspaceName(issue, {
|
|
projectWorkspaceById: new Map([
|
|
[defaultWorkspace.id, defaultWorkspace],
|
|
[secondaryWorkspace.id, secondaryWorkspace],
|
|
]),
|
|
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
|
}),
|
|
).toBe("Secondary workspace");
|
|
|
|
issue.projectWorkspaceId = null;
|
|
expect(
|
|
resolveIssueWorkspaceName(issue, {
|
|
projectWorkspaceById: new Map([
|
|
[defaultWorkspace.id, defaultWorkspace],
|
|
[secondaryWorkspace.id, secondaryWorkspace],
|
|
]),
|
|
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
|
}),
|
|
).toBeNull();
|
|
|
|
issue.executionWorkspaceId = "execution-workspace-shared-default";
|
|
issue.projectWorkspaceId = defaultWorkspace.id;
|
|
expect(
|
|
resolveIssueWorkspaceName(issue, {
|
|
executionWorkspaceById: new Map([[
|
|
issue.executionWorkspaceId,
|
|
makeExecutionWorkspace({
|
|
id: issue.executionWorkspaceId,
|
|
mode: "shared_workspace",
|
|
strategyType: "project_primary",
|
|
projectWorkspaceId: defaultWorkspace.id,
|
|
name: "PAP-1067",
|
|
}),
|
|
]]),
|
|
projectWorkspaceById: new Map([
|
|
[defaultWorkspace.id, defaultWorkspace],
|
|
[secondaryWorkspace.id, secondaryWorkspace],
|
|
]),
|
|
defaultProjectWorkspaceIdByProjectId: new Map([[issue.projectId!, defaultWorkspace.id]]),
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("maps legacy new-tab storage to mine", () => {
|
|
localStorage.setItem("paperclip:inbox:last-tab", "new");
|
|
expect(loadLastInboxTab()).toBe("mine");
|
|
});
|
|
|
|
it("enables swipe archive only on the mine tab", () => {
|
|
expect(isMineInboxTab("mine")).toBe(true);
|
|
expect(isMineInboxTab("recent")).toBe(false);
|
|
expect(isMineInboxTab("unread")).toBe(false);
|
|
expect(isMineInboxTab("all")).toBe(false);
|
|
});
|
|
|
|
it("anchors Mine selection to the first available inbox row", () => {
|
|
expect(resolveInboxSelectionIndex(-1, 3)).toBe(-1);
|
|
expect(resolveInboxSelectionIndex(5, 3)).toBe(2);
|
|
expect(resolveInboxSelectionIndex(1, 0)).toBe(-1);
|
|
});
|
|
|
|
it("selects the first row only after keyboard navigation starts", () => {
|
|
expect(getInboxKeyboardSelectionIndex(-1, 3, "next")).toBe(0);
|
|
expect(getInboxKeyboardSelectionIndex(-1, 3, "previous")).toBe(0);
|
|
expect(getInboxKeyboardSelectionIndex(0, 3, "next")).toBe(1);
|
|
expect(getInboxKeyboardSelectionIndex(0, 3, "previous")).toBe(0);
|
|
});
|
|
});
|