perf(renderer): skip unchanged kanban task cards
This commit is contained in:
parent
a491cd6c1c
commit
c02565b07d
2 changed files with 82 additions and 3 deletions
|
|
@ -10,6 +10,7 @@ const unreadBadgeMock = vi.hoisted(() => ({
|
||||||
|
|
||||||
const unreadCommentCountMock = vi.hoisted(() => ({
|
const unreadCommentCountMock = vi.hoisted(() => ({
|
||||||
value: 0,
|
value: 0,
|
||||||
|
calls: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@renderer/components/team/MemberBadge', () => ({
|
vi.mock('@renderer/components/team/MemberBadge', () => ({
|
||||||
|
|
@ -71,7 +72,10 @@ vi.mock('@renderer/hooks/useTheme', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@renderer/hooks/useUnreadCommentCount', () => ({
|
vi.mock('@renderer/hooks/useUnreadCommentCount', () => ({
|
||||||
useUnreadCommentCount: () => unreadCommentCountMock.value,
|
useUnreadCommentCount: () => {
|
||||||
|
unreadCommentCountMock.calls += 1;
|
||||||
|
return unreadCommentCountMock.value;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/* eslint-enable @typescript-eslint/naming-convention -- Re-enable after component mocks. */
|
/* eslint-enable @typescript-eslint/naming-convention -- Re-enable after component mocks. */
|
||||||
|
|
@ -188,6 +192,7 @@ async function rerenderStrictTaskCard(
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
unreadBadgeMock.props.length = 0;
|
unreadBadgeMock.props.length = 0;
|
||||||
unreadCommentCountMock.value = 0;
|
unreadCommentCountMock.value = 0;
|
||||||
|
unreadCommentCountMock.calls = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderTaskCard(
|
async function renderTaskCard(
|
||||||
|
|
@ -211,6 +216,56 @@ describe('KanbanTaskCard comment badge pulse', () => {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('skips rerender when refreshed task objects keep the same snapshot', async () => {
|
||||||
|
const taskMap = new Map();
|
||||||
|
const memberColorMap = new Map([['alice', 'blue']]);
|
||||||
|
const { root } = await renderTaskCard({
|
||||||
|
task: { ...baseTask, comments: [] },
|
||||||
|
taskMap,
|
||||||
|
memberColorMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(unreadCommentCountMock.calls).toBeGreaterThan(0);
|
||||||
|
unreadCommentCountMock.calls = 0;
|
||||||
|
|
||||||
|
await rerenderTaskCard(root, {
|
||||||
|
task: { ...baseTask, comments: [] },
|
||||||
|
taskMap,
|
||||||
|
memberColorMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(unreadCommentCountMock.calls).toBe(0);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await flushReact();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rerenders when a hidden task field changes so click handlers stay current', async () => {
|
||||||
|
const taskMap = new Map();
|
||||||
|
const memberColorMap = new Map([['alice', 'blue']]);
|
||||||
|
const { root } = await renderTaskCard({
|
||||||
|
task: { ...baseTask, comments: [] },
|
||||||
|
taskMap,
|
||||||
|
memberColorMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
unreadCommentCountMock.calls = 0;
|
||||||
|
await rerenderTaskCard(root, {
|
||||||
|
task: { ...baseTask, comments: [], description: 'Updated hidden details' },
|
||||||
|
taskMap,
|
||||||
|
memberColorMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(unreadCommentCountMock.calls).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await flushReact();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('does not pulse on initial render with existing comments', async () => {
|
it('does not pulse on initial render with existing comments', async () => {
|
||||||
const { host, root } = await renderTaskCard({
|
const { host, root } = await renderTaskCard({
|
||||||
task: { ...baseTask, comments: [createComment('comment-1')] },
|
task: { ...baseTask, comments: [createComment('comment-1')] },
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,30 @@ interface CommentPulseSyncAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_TASK_COMMENTS: readonly TaskComment[] = [];
|
const EMPTY_TASK_COMMENTS: readonly TaskComment[] = [];
|
||||||
|
const taskCardSignatureCache = new WeakMap<TeamTaskWithKanban, string>();
|
||||||
|
|
||||||
|
function getTaskCardSignature(task: TeamTaskWithKanban): string {
|
||||||
|
const cached = taskCardSignatureCache.get(task);
|
||||||
|
if (cached !== undefined) return cached;
|
||||||
|
|
||||||
|
const signature = JSON.stringify(task);
|
||||||
|
taskCardSignatureCache.set(task, signature);
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
function areKanbanTaskStatesEqual(
|
||||||
|
prev: KanbanTaskState | undefined,
|
||||||
|
next: KanbanTaskState | undefined
|
||||||
|
): boolean {
|
||||||
|
if (prev === next) return true;
|
||||||
|
if (!prev || !next) return !prev && !next;
|
||||||
|
return (
|
||||||
|
prev.column === next.column &&
|
||||||
|
prev.reviewer === next.reviewer &&
|
||||||
|
prev.errorDescription === next.errorDescription &&
|
||||||
|
prev.movedAt === next.movedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function createCommentPulseState(
|
function createCommentPulseState(
|
||||||
taskKey: string,
|
taskKey: string,
|
||||||
|
|
@ -652,10 +676,10 @@ export const KanbanTaskCard = memo(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(prev, next) =>
|
(prev, next) =>
|
||||||
prev.task === next.task &&
|
getTaskCardSignature(prev.task) === getTaskCardSignature(next.task) &&
|
||||||
prev.teamName === next.teamName &&
|
prev.teamName === next.teamName &&
|
||||||
prev.columnId === next.columnId &&
|
prev.columnId === next.columnId &&
|
||||||
prev.kanbanTaskState === next.kanbanTaskState &&
|
areKanbanTaskStatesEqual(prev.kanbanTaskState, next.kanbanTaskState) &&
|
||||||
prev.hasReviewers === next.hasReviewers &&
|
prev.hasReviewers === next.hasReviewers &&
|
||||||
prev.compact === next.compact &&
|
prev.compact === next.compact &&
|
||||||
prev.taskMap === next.taskMap &&
|
prev.taskMap === next.taskMap &&
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue