691 lines
20 KiB
TypeScript
691 lines
20 KiB
TypeScript
/* eslint-disable @typescript-eslint/naming-convention -- Component mocks mirror PascalCase exports. */
|
|
import React, { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const unreadBadgeMock = vi.hoisted(() => ({
|
|
props: [] as { unreadCount: number; totalCount: number; pulseKey?: number }[],
|
|
}));
|
|
|
|
const unreadCommentCountMock = vi.hoisted(() => ({
|
|
value: 0,
|
|
calls: 0,
|
|
}));
|
|
|
|
vi.mock('@renderer/components/team/MemberBadge', () => ({
|
|
MemberBadge: ({ name }: { name: string }) => React.createElement('span', null, name),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/team/UnreadCommentsBadge', () => ({
|
|
UnreadCommentsBadge: (props: { unreadCount: number; totalCount: number; pulseKey?: number }) => {
|
|
unreadBadgeMock.props.push(props);
|
|
return React.createElement('span', {
|
|
className: (props.pulseKey ?? 0) > 0 ? 'kanban-comment-badge-pulse' : '',
|
|
'data-pulse-key': props.pulseKey ?? 0,
|
|
'data-testid': 'unread-comments-badge',
|
|
});
|
|
},
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/button', () => ({
|
|
Button: ({
|
|
children,
|
|
className,
|
|
onClick,
|
|
disabled,
|
|
'aria-label': ariaLabel,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
disabled?: boolean;
|
|
'aria-label'?: string;
|
|
}) =>
|
|
React.createElement(
|
|
'button',
|
|
{ className, onClick, disabled, 'aria-label': ariaLabel, type: 'button' },
|
|
children
|
|
),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/popover', () => ({
|
|
Popover: ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(React.Fragment, null, children),
|
|
PopoverTrigger: ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(React.Fragment, null, children),
|
|
PopoverContent: ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement('div', null, children),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/tooltip', () => ({
|
|
Tooltip: ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(React.Fragment, null, children),
|
|
TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement(React.Fragment, null, children),
|
|
TooltipContent: ({ children }: { children: React.ReactNode }) =>
|
|
React.createElement('div', null, children),
|
|
}));
|
|
|
|
vi.mock('@renderer/hooks/useTheme', () => ({
|
|
useTheme: () => ({ isLight: false }),
|
|
}));
|
|
|
|
vi.mock('@renderer/hooks/useUnreadCommentCount', () => ({
|
|
useUnreadCommentCount: () => {
|
|
unreadCommentCountMock.calls += 1;
|
|
return unreadCommentCountMock.value;
|
|
},
|
|
}));
|
|
|
|
/* eslint-enable @typescript-eslint/naming-convention -- Re-enable after component mocks. */
|
|
|
|
import { KanbanTaskCard } from './KanbanTaskCard';
|
|
|
|
import type { TaskComment, TeamTaskWithKanban } from '@shared/types/team';
|
|
|
|
const baseTask: TeamTaskWithKanban = {
|
|
id: 'task-1',
|
|
displayId: 'abcd1234',
|
|
subject: 'Implement safer onboarding flow',
|
|
owner: 'alice',
|
|
reviewer: '',
|
|
status: 'in_progress',
|
|
changePresence: 'unknown',
|
|
comments: [],
|
|
blockedBy: [],
|
|
blocks: [],
|
|
workIntervals: [],
|
|
historyEvents: [],
|
|
createdAt: '2026-04-18T10:00:00.000Z',
|
|
updatedAt: '2026-04-18T10:10:00.000Z',
|
|
} as unknown as TeamTaskWithKanban;
|
|
|
|
const noop = (): void => undefined;
|
|
|
|
function createComment(id: string, author = 'teammate'): TaskComment {
|
|
return {
|
|
id,
|
|
author,
|
|
text: `Comment ${id}`,
|
|
createdAt: '2026-04-18T10:20:00.000Z',
|
|
type: 'regular',
|
|
};
|
|
}
|
|
|
|
function createTaskCardElement(
|
|
props: Partial<React.ComponentProps<typeof KanbanTaskCard>> = {}
|
|
): React.ReactElement {
|
|
return React.createElement(KanbanTaskCard, {
|
|
task: baseTask,
|
|
teamName: 'my-team',
|
|
columnId: 'in_progress',
|
|
hasReviewers: true,
|
|
compact: false,
|
|
taskMap: new Map(),
|
|
memberColorMap: new Map([['alice', 'blue']]),
|
|
onRequestReview: noop,
|
|
onApprove: noop,
|
|
onRequestChanges: noop,
|
|
onMoveBackToDone: noop,
|
|
onStartTask: noop,
|
|
onCompleteTask: noop,
|
|
onCancelTask: noop,
|
|
onViewChanges: noop,
|
|
...props,
|
|
});
|
|
}
|
|
|
|
function getLastUnreadBadgeProps(): { unreadCount: number; totalCount: number; pulseKey?: number } {
|
|
const props = unreadBadgeMock.props[unreadBadgeMock.props.length - 1];
|
|
if (!props) throw new Error('UnreadCommentsBadge was not rendered');
|
|
return props;
|
|
}
|
|
|
|
async function flushReact(): Promise<void> {
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
}
|
|
|
|
async function rerenderTaskCard(
|
|
root: ReturnType<typeof createRoot>,
|
|
props: Partial<React.ComponentProps<typeof KanbanTaskCard>> = {}
|
|
): Promise<void> {
|
|
await act(async () => {
|
|
root.render(createTaskCardElement(props));
|
|
await flushReact();
|
|
});
|
|
}
|
|
|
|
function createStrictTaskCardElement(
|
|
props: Partial<React.ComponentProps<typeof KanbanTaskCard>> = {}
|
|
): React.ReactElement {
|
|
return React.createElement(React.StrictMode, null, createTaskCardElement(props));
|
|
}
|
|
|
|
async function renderStrictTaskCard(
|
|
props: Partial<React.ComponentProps<typeof KanbanTaskCard>> = {}
|
|
): Promise<{ host: HTMLDivElement; root: ReturnType<typeof createRoot> }> {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(createStrictTaskCardElement(props));
|
|
await flushReact();
|
|
});
|
|
|
|
return { host, root };
|
|
}
|
|
|
|
async function rerenderStrictTaskCard(
|
|
root: ReturnType<typeof createRoot>,
|
|
props: Partial<React.ComponentProps<typeof KanbanTaskCard>> = {}
|
|
): Promise<void> {
|
|
await act(async () => {
|
|
root.render(createStrictTaskCardElement(props));
|
|
await flushReact();
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
unreadBadgeMock.props.length = 0;
|
|
unreadCommentCountMock.value = 0;
|
|
unreadCommentCountMock.calls = 0;
|
|
});
|
|
|
|
async function renderTaskCard(
|
|
props: Partial<React.ComponentProps<typeof KanbanTaskCard>> = {}
|
|
): Promise<{ host: HTMLDivElement; root: ReturnType<typeof createRoot> }> {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(createTaskCardElement(props));
|
|
await flushReact();
|
|
});
|
|
|
|
return { host, root };
|
|
}
|
|
|
|
describe('KanbanTaskCard comment badge pulse', () => {
|
|
afterEach(() => {
|
|
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('skips rerender when an unrelated taskMap entry changes', async () => {
|
|
const memberColorMap = new Map([['alice', 'blue']]);
|
|
const { root } = await renderTaskCard({
|
|
task: { ...baseTask, blockedBy: [], blocks: [], comments: [] },
|
|
taskMap: new Map([['other-task', { ...baseTask, id: 'other-task', subject: 'Other task' }]]),
|
|
memberColorMap,
|
|
});
|
|
|
|
unreadCommentCountMock.calls = 0;
|
|
await rerenderTaskCard(root, {
|
|
task: { ...baseTask, blockedBy: [], blocks: [], comments: [] },
|
|
taskMap: new Map([
|
|
['other-task', { ...baseTask, id: 'other-task', subject: 'Updated unrelated task' }],
|
|
]),
|
|
memberColorMap,
|
|
});
|
|
|
|
expect(unreadCommentCountMock.calls).toBe(0);
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await flushReact();
|
|
});
|
|
});
|
|
|
|
it('rerenders when a displayed dependency task changes', async () => {
|
|
const memberColorMap = new Map([['alice', 'blue']]);
|
|
const blockedTask = { ...baseTask, id: 'dep-1', displayId: 'dep1', subject: 'Dependency A' };
|
|
const { root } = await renderTaskCard({
|
|
task: { ...baseTask, blockedBy: ['dep-1'], blocks: [], comments: [] },
|
|
taskMap: new Map([['dep-1', blockedTask]]),
|
|
memberColorMap,
|
|
});
|
|
|
|
unreadCommentCountMock.calls = 0;
|
|
await rerenderTaskCard(root, {
|
|
task: { ...baseTask, blockedBy: ['dep-1'], blocks: [], comments: [] },
|
|
taskMap: new Map([['dep-1', { ...blockedTask, subject: 'Dependency B', status: 'done' }]]),
|
|
memberColorMap,
|
|
});
|
|
|
|
expect(unreadCommentCountMock.calls).toBeGreaterThan(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 () => {
|
|
const { host, root } = await renderTaskCard({
|
|
task: { ...baseTask, comments: [createComment('comment-1')] },
|
|
});
|
|
|
|
expect(getLastUnreadBadgeProps().pulseKey ?? 0).toBe(0);
|
|
expect(host.querySelector('.kanban-comment-badge-pulse')).toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await flushReact();
|
|
});
|
|
});
|
|
|
|
it('pulses when a new non-user comment arrives', async () => {
|
|
const firstComment = createComment('comment-1');
|
|
const { host, root } = await renderTaskCard({
|
|
task: { ...baseTask, comments: [firstComment] },
|
|
});
|
|
|
|
unreadBadgeMock.props.length = 0;
|
|
await rerenderTaskCard(root, {
|
|
task: { ...baseTask, comments: [firstComment, createComment('comment-2')] },
|
|
});
|
|
|
|
expect(getLastUnreadBadgeProps().pulseKey).toBe(1);
|
|
expect(host.querySelector('.kanban-comment-badge-pulse')).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await flushReact();
|
|
});
|
|
});
|
|
|
|
it('pulses when the first non-user comment arrives', async () => {
|
|
const { host, root } = await renderTaskCard({
|
|
task: { ...baseTask, comments: [] },
|
|
});
|
|
|
|
unreadBadgeMock.props.length = 0;
|
|
await rerenderTaskCard(root, {
|
|
task: { ...baseTask, comments: [createComment('comment-1')] },
|
|
});
|
|
|
|
expect(getLastUnreadBadgeProps().pulseKey).toBe(1);
|
|
expect(host.querySelector('.kanban-comment-badge-pulse')).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await flushReact();
|
|
});
|
|
});
|
|
|
|
it('does not double-pulse under React StrictMode', async () => {
|
|
const firstComment = createComment('comment-1');
|
|
const { root } = await renderStrictTaskCard({
|
|
task: { ...baseTask, comments: [firstComment] },
|
|
});
|
|
|
|
unreadBadgeMock.props.length = 0;
|
|
await rerenderStrictTaskCard(root, {
|
|
task: { ...baseTask, comments: [firstComment, createComment('comment-2')] },
|
|
});
|
|
|
|
expect(getLastUnreadBadgeProps().pulseKey).toBe(1);
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await flushReact();
|
|
});
|
|
});
|
|
|
|
it('restarts the pulse when another non-user comment arrives', async () => {
|
|
const firstComment = createComment('comment-1');
|
|
const secondComment = createComment('comment-2');
|
|
const { root } = await renderTaskCard({
|
|
task: { ...baseTask, comments: [firstComment] },
|
|
});
|
|
|
|
await rerenderTaskCard(root, {
|
|
task: { ...baseTask, comments: [firstComment, secondComment] },
|
|
});
|
|
expect(getLastUnreadBadgeProps().pulseKey).toBe(1);
|
|
|
|
unreadBadgeMock.props.length = 0;
|
|
await rerenderTaskCard(root, {
|
|
task: {
|
|
...baseTask,
|
|
comments: [firstComment, secondComment, createComment('comment-3')],
|
|
},
|
|
});
|
|
|
|
expect(getLastUnreadBadgeProps().pulseKey).toBe(2);
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await flushReact();
|
|
});
|
|
});
|
|
|
|
it('does not pulse when the new comment belongs to the user', async () => {
|
|
const firstComment = createComment('comment-1');
|
|
const { host, root } = await renderTaskCard({
|
|
task: { ...baseTask, comments: [firstComment] },
|
|
});
|
|
|
|
unreadBadgeMock.props.length = 0;
|
|
await rerenderTaskCard(root, {
|
|
task: { ...baseTask, comments: [firstComment, createComment('comment-2', 'user')] },
|
|
});
|
|
|
|
expect(getLastUnreadBadgeProps().pulseKey ?? 0).toBe(0);
|
|
expect(host.querySelector('.kanban-comment-badge-pulse')).toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await flushReact();
|
|
});
|
|
});
|
|
|
|
it('does not pulse when only the unread count changes', async () => {
|
|
const taskWithComment = { ...baseTask, comments: [createComment('comment-1')] };
|
|
const { root } = await renderTaskCard({ task: taskWithComment });
|
|
|
|
unreadBadgeMock.props.length = 0;
|
|
unreadCommentCountMock.value = 1;
|
|
await rerenderTaskCard(root, { task: taskWithComment });
|
|
|
|
const props = getLastUnreadBadgeProps();
|
|
expect(props.unreadCount).toBe(1);
|
|
expect(props.pulseKey ?? 0).toBe(0);
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await flushReact();
|
|
});
|
|
});
|
|
|
|
it('does not reuse an old pulse when the card instance switches tasks', async () => {
|
|
const firstComment = createComment('comment-1');
|
|
const secondComment = createComment('comment-2');
|
|
const taskWithPulse = { ...baseTask, comments: [firstComment] };
|
|
const { root } = await renderTaskCard({ task: taskWithPulse });
|
|
|
|
await rerenderTaskCard(root, {
|
|
task: { ...baseTask, comments: [firstComment, secondComment] },
|
|
});
|
|
expect(getLastUnreadBadgeProps().pulseKey).toBe(1);
|
|
|
|
unreadBadgeMock.props.length = 0;
|
|
await rerenderTaskCard(root, {
|
|
task: {
|
|
...baseTask,
|
|
id: 'task-2',
|
|
displayId: 'efgh5678',
|
|
comments: [createComment('task-2-comment')],
|
|
},
|
|
});
|
|
expect(getLastUnreadBadgeProps().pulseKey ?? 0).toBe(0);
|
|
|
|
unreadBadgeMock.props.length = 0;
|
|
await rerenderTaskCard(root, {
|
|
task: { ...baseTask, comments: [firstComment, secondComment] },
|
|
});
|
|
expect(getLastUnreadBadgeProps().pulseKey ?? 0).toBe(0);
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await flushReact();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('KanbanTaskCard change badge', () => {
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
});
|
|
|
|
it('does not render a No changes badge when changePresence is no_changes', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(KanbanTaskCard, {
|
|
task: { ...baseTask, changePresence: 'no_changes' },
|
|
teamName: 'my-team',
|
|
columnId: 'in_progress',
|
|
hasReviewers: true,
|
|
compact: false,
|
|
taskMap: new Map(),
|
|
memberColorMap: new Map([['alice', 'blue']]),
|
|
onRequestReview: noop,
|
|
onApprove: noop,
|
|
onRequestChanges: noop,
|
|
onMoveBackToDone: noop,
|
|
onStartTask: noop,
|
|
onCompleteTask: noop,
|
|
onCancelTask: noop,
|
|
onViewChanges: noop,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).not.toContain('No changes');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('still renders the Changes action when changePresence is has_changes', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(KanbanTaskCard, {
|
|
task: { ...baseTask, changePresence: 'has_changes' },
|
|
teamName: 'my-team',
|
|
columnId: 'in_progress',
|
|
hasReviewers: true,
|
|
compact: false,
|
|
taskMap: new Map(),
|
|
memberColorMap: new Map([['alice', 'blue']]),
|
|
onRequestReview: noop,
|
|
onApprove: noop,
|
|
onRequestChanges: noop,
|
|
onMoveBackToDone: noop,
|
|
onStartTask: noop,
|
|
onCompleteTask: noop,
|
|
onCancelTask: noop,
|
|
onViewChanges: noop,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.querySelector('[aria-label="Changes"]')).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('renders a Changes attention action when changePresence needs attention', async () => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(KanbanTaskCard, {
|
|
task: { ...baseTask, changePresence: 'needs_attention' },
|
|
teamName: 'my-team',
|
|
columnId: 'in_progress',
|
|
hasReviewers: true,
|
|
compact: false,
|
|
taskMap: new Map(),
|
|
memberColorMap: new Map([['alice', 'blue']]),
|
|
onRequestReview: noop,
|
|
onApprove: noop,
|
|
onRequestChanges: noop,
|
|
onMoveBackToDone: noop,
|
|
onStartTask: noop,
|
|
onCompleteTask: noop,
|
|
onCancelTask: noop,
|
|
onViewChanges: noop,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.querySelector('[aria-label="Changes need attention"]')).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('KanbanTaskCard blocked border', () => {
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
});
|
|
|
|
it('highlights blocked tasks outside final columns', async () => {
|
|
const { host, root } = await renderTaskCard({
|
|
task: { ...baseTask, blockedBy: ['task-2'] },
|
|
columnId: 'in_progress',
|
|
});
|
|
|
|
const card = host.querySelector('[data-task-id="task-1"]');
|
|
expect(card?.className).toContain('kanban-task-card');
|
|
expect(card?.className).toContain('border-yellow-500/30');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it.each(['done', 'approved'] as const)(
|
|
'does not highlight blocked tasks in %s',
|
|
async (columnId) => {
|
|
const { host, root } = await renderTaskCard({
|
|
task: { ...baseTask, blockedBy: ['task-2'] },
|
|
columnId,
|
|
});
|
|
|
|
const card = host.querySelector('[data-task-id="task-1"]');
|
|
expect(card?.className).not.toContain('border-yellow-500/30');
|
|
expect(card?.className).toContain('border-[var(--color-border)]');
|
|
expect(host.textContent).toContain('Blocked by');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('KanbanTaskCard live log indicator', () => {
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
});
|
|
|
|
it('shows the live log indicator only when task log activity is active', async () => {
|
|
const { host, root } = await renderTaskCard({ hasLiveTaskLogs: true });
|
|
|
|
expect(host.querySelector('[aria-label="Task logs active"]')).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(KanbanTaskCard, {
|
|
task: baseTask,
|
|
teamName: 'my-team',
|
|
columnId: 'in_progress',
|
|
hasReviewers: true,
|
|
compact: false,
|
|
taskMap: new Map(),
|
|
memberColorMap: new Map([['alice', 'blue']]),
|
|
onRequestReview: noop,
|
|
onApprove: noop,
|
|
onRequestChanges: noop,
|
|
onMoveBackToDone: noop,
|
|
onStartTask: noop,
|
|
onCompleteTask: noop,
|
|
onCancelTask: noop,
|
|
onViewChanges: noop,
|
|
hasLiveTaskLogs: false,
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.querySelector('[aria-label="Task logs active"]')).toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
});
|