mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
1143 lines
41 KiB
TypeScript
1143 lines
41 KiB
TypeScript
/**
|
|
* TaskCard Component Tests
|
|
*
|
|
* Tests for the TaskCard UI component including:
|
|
* - Task card creation with various options
|
|
* - Status and priority indicators
|
|
* - Interactive elements (checkboxes, context menus)
|
|
* - Recurring task UI behavior
|
|
* - Event handlers and user interactions
|
|
* - Task card updates and reconciliation
|
|
* - Context menu functionality
|
|
* - Error handling and edge cases
|
|
*/
|
|
|
|
import {
|
|
createTaskCard,
|
|
updateTaskCard,
|
|
showTaskContextMenu,
|
|
showDeleteConfirmationModal,
|
|
TaskCardOptions,
|
|
DEFAULT_TASK_CARD_OPTIONS
|
|
} from '../../../src/ui/TaskCard';
|
|
|
|
import { TaskInfo } from '../../../src/types';
|
|
import { TaskFactory } from '../../helpers/mock-factories';
|
|
import { MockObsidian, TFile, Menu, Notice, Modal, App } from '../../__mocks__/obsidian';
|
|
|
|
// Mock external dependencies
|
|
jest.mock('obsidian');
|
|
jest.mock('date-fns', () => ({
|
|
format: jest.fn((date: Date, formatStr: string) => {
|
|
if (formatStr === 'yyyy-MM-dd') {
|
|
return date.toISOString().split('T')[0];
|
|
}
|
|
return date.toISOString();
|
|
})
|
|
}));
|
|
|
|
// Mock helper functions
|
|
jest.mock('../../../src/utils/helpers', () => ({
|
|
calculateTotalTimeSpent: jest.fn((entries) => entries?.length ? entries.length * 30 : 0),
|
|
getEffectiveTaskStatus: jest.fn((task, date) => {
|
|
if (task.recurrence && task.complete_instances?.includes('2025-01-15')) {
|
|
return 'done';
|
|
}
|
|
return task.status || 'open';
|
|
}),
|
|
shouldUseRecurringTaskUI: jest.fn((task) => !!task.recurrence),
|
|
getRecurringTaskCompletionText: jest.fn(() => 'Not completed for this date'),
|
|
getRecurrenceDisplayText: jest.fn((recurrence) => 'Daily'),
|
|
filterEmptyProjects: jest.fn((projects) => projects?.filter(p => p && p.trim()) || [])
|
|
}));
|
|
|
|
jest.mock('../../../src/utils/dateUtils', () => ({
|
|
isTodayTimeAware: jest.fn((date) => date === '2025-01-15'),
|
|
isOverdueTimeAware: jest.fn((date) => date === '2020-01-01'),
|
|
formatDateTimeForDisplay: jest.fn((date, options) => {
|
|
if (options?.dateFormat === '') return options?.userTimeFormat === '12' ? '2:30 PM' : '14:30';
|
|
if (date === '2025-01-15T14:30:00') {
|
|
return options?.userTimeFormat === '12' ? 'Jan 15, 2025 2:30 PM' : 'Jan 15, 2025 14:30';
|
|
}
|
|
if (date === '2025-01-15') return 'Jan 15, 2025';
|
|
return 'Jan 15, 2025';
|
|
}),
|
|
getDatePart: jest.fn((date) => date?.split('T')[0] || ''),
|
|
getTimePart: jest.fn((date) => date?.includes('T') ? date.split('T')[1]?.split(':').slice(0, 2).join(':') : null),
|
|
formatDateForStorage: jest.fn((value: Date | string) => {
|
|
if (value instanceof Date) {
|
|
return value.toISOString().split('T')[0];
|
|
}
|
|
return value?.split('T')[0] || '';
|
|
})
|
|
}));
|
|
|
|
// Mock TaskContextMenu to use the mocked Menu internally
|
|
let lastMenuInstance: any = null;
|
|
|
|
jest.mock('../../../src/components/TaskContextMenu', () => {
|
|
return {
|
|
TaskContextMenu: jest.fn().mockImplementation((options) => {
|
|
// Create a new Menu instance (which will be mocked)
|
|
const { Menu } = require('obsidian');
|
|
const menuInstance = new Menu();
|
|
lastMenuInstance = menuInstance;
|
|
|
|
// Simulate basic menu building - always add at least one item
|
|
menuInstance.addItem(() => {});
|
|
|
|
// Simulate the actual TaskContextMenu behavior for recurring tasks
|
|
if (options.task.recurrence) {
|
|
menuInstance.addSeparator();
|
|
}
|
|
|
|
// Simulate the actual TaskContextMenu behavior
|
|
const mockTaskContextMenu = {
|
|
show: jest.fn((event) => {
|
|
return menuInstance.showAtMouseEvent(event);
|
|
})
|
|
};
|
|
|
|
return mockTaskContextMenu;
|
|
})
|
|
};
|
|
});
|
|
|
|
describe('TaskCard Component', () => {
|
|
let mockPlugin: any;
|
|
let mockApp: any;
|
|
let container: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
MockObsidian.reset();
|
|
|
|
// Create DOM container
|
|
document.body.innerHTML = '';
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
|
|
// Mock plugin
|
|
mockApp = new App();
|
|
mockPlugin = {
|
|
app: mockApp,
|
|
selectedDate: new Date('2025-01-15'),
|
|
fieldMapper: {
|
|
isPropertyForField: jest.fn(() => false),
|
|
toUserField: jest.fn((field) => field),
|
|
toInternalField: jest.fn((field) => field),
|
|
getMapping: jest.fn(() => ({
|
|
status: 'status',
|
|
priority: 'priority',
|
|
due: 'due',
|
|
scheduled: 'scheduled',
|
|
title: 'title',
|
|
tags: 'tags',
|
|
contexts: 'contexts',
|
|
projects: 'projects',
|
|
})),
|
|
},
|
|
statusManager: {
|
|
isCompletedStatus: jest.fn((status) => status === 'done'),
|
|
getStatusConfig: jest.fn((status) => ({
|
|
value: status,
|
|
label: status,
|
|
color: '#666666'
|
|
})),
|
|
getAllStatuses: jest.fn(() => [
|
|
{ value: 'open', label: 'Open' },
|
|
{ value: 'done', label: 'Done' }
|
|
]),
|
|
getNonCompletionStatuses: jest.fn(() => [
|
|
{ value: 'open', label: 'Open' },
|
|
{ value: 'in-progress', label: 'In Progress' }
|
|
]),
|
|
getNextStatus: jest.fn(() => 'done'),
|
|
getCompletedStatuses: jest.fn(() => ['done'])
|
|
},
|
|
priorityManager: {
|
|
getPriorityConfig: jest.fn((priority) => ({
|
|
value: priority,
|
|
label: priority,
|
|
color: '#ff0000'
|
|
})),
|
|
getPrioritiesByWeight: jest.fn(() => [
|
|
{ value: 'high', label: 'High' },
|
|
{ value: 'normal', label: 'Normal' }
|
|
])
|
|
},
|
|
getActiveTimeSession: jest.fn(() => null),
|
|
toggleTaskStatus: jest.fn(),
|
|
toggleRecurringTaskComplete: jest.fn(),
|
|
updateTaskProperty: jest.fn(),
|
|
openTaskEditModal: jest.fn(),
|
|
openDueDateModal: jest.fn(),
|
|
openScheduledDateModal: jest.fn(),
|
|
startTimeTracking: jest.fn(),
|
|
stopTimeTracking: jest.fn(),
|
|
toggleTaskArchive: jest.fn(),
|
|
formatTime: jest.fn((minutes) => `${minutes}m`),
|
|
cacheManager: {
|
|
getTaskInfo: jest.fn()
|
|
},
|
|
taskService: {
|
|
deleteTask: jest.fn()
|
|
},
|
|
projectSubtasksService: {
|
|
isTaskUsedAsProject: jest.fn().mockResolvedValue(false),
|
|
isTaskUsedAsProjectSync: jest.fn().mockReturnValue(false)
|
|
},
|
|
expandedProjectsService: {
|
|
isExpanded: jest.fn(() => false),
|
|
toggle: jest.fn(() => true),
|
|
},
|
|
i18n: {
|
|
translate: jest.fn((key, vars) => {
|
|
if (key === 'ui.taskCard.priorityAriaLabel') {
|
|
return `Priority: ${vars?.label ?? ''}`;
|
|
}
|
|
if (key === 'ui.taskCard.taskOptions') {
|
|
return 'Task options';
|
|
}
|
|
if (key === 'ui.taskCard.blockingToggle') {
|
|
return `Blocking ${vars?.count ?? 0}`;
|
|
}
|
|
return key;
|
|
}),
|
|
},
|
|
settings: {
|
|
singleClickAction: 'edit',
|
|
doubleClickAction: 'none',
|
|
showExpandableSubtasks: true,
|
|
subtaskChevronPosition: 'right',
|
|
calendarViewSettings: {
|
|
timeFormat: '12' // Default to 12-hour format for test consistency
|
|
}
|
|
}
|
|
};
|
|
|
|
// Ensure app has proper mock methods - use the same mockApp variable
|
|
mockApp.vault = mockApp.vault || {};
|
|
mockApp.vault.getAbstractFileByPath = jest.fn();
|
|
mockApp.workspace = mockApp.workspace || {};
|
|
mockApp.workspace.getLeaf = jest.fn().mockReturnValue({
|
|
openFile: jest.fn()
|
|
});
|
|
mockApp.workspace.openLinkText = jest.fn();
|
|
mockApp.workspace.trigger = jest.fn();
|
|
mockApp.metadataCache = mockApp.metadataCache || {};
|
|
mockApp.metadataCache.getFirstLinkpathDest = jest.fn();
|
|
|
|
// Mock console methods
|
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
jest.spyOn(console, 'debug').mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
document.body.innerHTML = '';
|
|
});
|
|
|
|
describe('createTaskCard', () => {
|
|
it('should create basic task card', () => {
|
|
const task = TaskFactory.createTask({
|
|
title: 'Test Task',
|
|
status: 'open',
|
|
priority: 'high'
|
|
});
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
expect(card).toBeInstanceOf(HTMLElement);
|
|
expect(card.classList.contains('task-card')).toBe(true);
|
|
expect(card.classList.contains('task-card--priority-high')).toBe(true);
|
|
expect(card.classList.contains('task-card--status-open')).toBe(true);
|
|
expect(card.dataset.taskPath).toBe(task.path);
|
|
expect(card.dataset.status).toBe('open');
|
|
});
|
|
|
|
it('should create task card with all default options', () => {
|
|
const task = TaskFactory.createTask();
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
// Should not have checkbox by default
|
|
expect(card.querySelector('.task-card__checkbox')).toBeNull();
|
|
|
|
// Should have status dot
|
|
expect(card.querySelector('.task-card__status-dot')).toBeTruthy();
|
|
|
|
// Should have title
|
|
const titleEl = card.querySelector('.task-card__title');
|
|
expect(titleEl?.textContent).toBe(task.title);
|
|
});
|
|
|
|
it.skip('should create task card with checkbox when enabled', () => {
|
|
const task = TaskFactory.createTask({ status: 'done' });
|
|
const options: Partial<TaskCardOptions> = { showCheckbox: true };
|
|
|
|
const card = createTaskCard(task, mockPlugin, undefined, options);
|
|
|
|
const checkbox = card.querySelector('.task-card__checkbox') as HTMLInputElement;
|
|
expect(checkbox).toBeTruthy();
|
|
expect(checkbox.checked).toBe(true);
|
|
expect(card.classList.contains('task-card--checkbox-enabled')).toBe(true);
|
|
});
|
|
|
|
it('should create recurring task card', () => {
|
|
const task = TaskFactory.createRecurringTask('FREQ=DAILY');
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
expect(card.classList.contains('task-card--recurring')).toBe(true);
|
|
expect(card.querySelector('.task-card__recurring-indicator')).toBeTruthy();
|
|
});
|
|
|
|
it('should create completed task card', () => {
|
|
const task = TaskFactory.createTask({ status: 'done' });
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
expect(card.classList.contains('task-card--completed')).toBe(true);
|
|
|
|
const titleEl = card.querySelector('.task-card__title');
|
|
expect(titleEl?.classList.contains('completed')).toBe(true);
|
|
});
|
|
|
|
it('should create archived task card', () => {
|
|
const task = TaskFactory.createTask({ archived: true });
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
expect(card.classList.contains('task-card--archived')).toBe(true);
|
|
});
|
|
|
|
it('should create actively tracked task card', () => {
|
|
const task = TaskFactory.createTask();
|
|
mockPlugin.getActiveTimeSession.mockReturnValue({
|
|
startTime: '2025-01-15T10:00:00Z'
|
|
});
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
expect(card.classList.contains('task-card--actively-tracked')).toBe(true);
|
|
});
|
|
|
|
it('should add has-projects class for tasks with projects (issue #355)', () => {
|
|
const taskWithProjects = TaskFactory.createTask({
|
|
projects: ['[[My Project]]', 'Another Project']
|
|
});
|
|
const cardWithProjects = createTaskCard(taskWithProjects, mockPlugin);
|
|
|
|
expect(cardWithProjects.classList.contains('task-card--has-projects')).toBe(true);
|
|
|
|
const taskWithoutProjects = TaskFactory.createTask({
|
|
projects: []
|
|
});
|
|
const cardWithoutProjects = createTaskCard(taskWithoutProjects, mockPlugin);
|
|
|
|
expect(cardWithoutProjects.classList.contains('task-card--has-projects')).toBe(false);
|
|
|
|
const taskWithEmptyProjects = TaskFactory.createTask({
|
|
projects: ['', ' ', null as any, undefined as any]
|
|
});
|
|
const cardWithEmptyProjects = createTaskCard(taskWithEmptyProjects, mockPlugin);
|
|
|
|
expect(cardWithEmptyProjects.classList.contains('task-card--has-projects')).toBe(false);
|
|
});
|
|
|
|
it('should apply priority and status colors', () => {
|
|
const task = TaskFactory.createTask({
|
|
status: 'open',
|
|
priority: 'high'
|
|
});
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
expect(card.style.getPropertyValue('--priority-color')).toBe('#ff0000');
|
|
expect(card.style.getPropertyValue('--current-status-color')).toBe('#666666');
|
|
});
|
|
|
|
it('should create priority indicator dot', () => {
|
|
const task = TaskFactory.createTask({ priority: 'high' });
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
const priorityDot = card.querySelector('.task-card__priority-dot') as HTMLElement;
|
|
expect(priorityDot).toBeTruthy();
|
|
expect(priorityDot.style.borderColor).toBe('rgb(255, 0, 0)');
|
|
expect(priorityDot.getAttribute('aria-label')).toBe('Priority: high');
|
|
});
|
|
|
|
it.skip('should create metadata line with various task properties', () => {
|
|
const task = TaskFactory.createTask({
|
|
due: '2025-01-15T14:30:00',
|
|
scheduled: '2025-01-15',
|
|
contexts: ['work', 'urgent'],
|
|
timeEstimate: 60,
|
|
timeEntries: [{ startTime: '2025-01-15T10:00:00Z', endTime: '2025-01-15T10:30:00Z' }]
|
|
});
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
const metadataLine = card.querySelector('.task-card__metadata');
|
|
|
|
expect(metadataLine?.textContent).toContain('Due:');
|
|
expect(metadataLine?.textContent).toContain('Scheduled:');
|
|
expect(metadataLine?.textContent).toContain('@work, @urgent');
|
|
// Time tracking info should only show when explicitly configured as visible properties
|
|
expect(metadataLine?.textContent).not.toContain('30m spent');
|
|
expect(metadataLine?.textContent).not.toContain('60m estimated');
|
|
});
|
|
|
|
it.skip('should show time tracking properties when explicitly enabled', () => {
|
|
const task = TaskFactory.createTask({
|
|
timeEstimate: 60,
|
|
timeEntries: [{ startTime: '2025-01-15T10:00:00Z', endTime: '2025-01-15T10:30:00Z' }],
|
|
totalTrackedTime: 30
|
|
});
|
|
|
|
// Test with timeEstimate and totalTrackedTime properties explicitly enabled
|
|
const visibleProperties = ['timeEstimate', 'totalTrackedTime'];
|
|
const card = createTaskCard(task, mockPlugin, visibleProperties);
|
|
const metadataLine = card.querySelector('.task-card__metadata');
|
|
|
|
expect(metadataLine?.textContent).toContain('60m estimated');
|
|
expect(metadataLine?.textContent).toContain('30m tracked');
|
|
});
|
|
|
|
it('should not show totalTrackedTime when value is 0', () => {
|
|
const task = TaskFactory.createTask({
|
|
totalTrackedTime: 0
|
|
});
|
|
|
|
// Enable totalTrackedTime property but value is 0
|
|
const visibleProperties = ['totalTrackedTime'];
|
|
const card = createTaskCard(task, mockPlugin, visibleProperties);
|
|
const metadataLine = card.querySelector('.task-card__metadata');
|
|
|
|
expect(metadataLine?.textContent).not.toContain('tracked');
|
|
});
|
|
|
|
it.skip('should create clickable project links for wikilink projects', () => {
|
|
const task = TaskFactory.createTask({
|
|
projects: ['[[Project A]]', '[[Project B]]', 'regular-project']
|
|
});
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
const metadataLine = card.querySelector('.task-card__metadata');
|
|
|
|
// Check that project links are rendered
|
|
expect(metadataLine?.textContent).toContain('+Project A');
|
|
expect(metadataLine?.textContent).toContain('+Project B');
|
|
expect(metadataLine?.textContent).toContain('+regular-project');
|
|
|
|
// Check that wikilink projects have clickable links
|
|
const projectLinks = card.querySelectorAll('.task-card__project-link');
|
|
expect(projectLinks.length).toBe(2); // Only wikilink projects should have clickable links
|
|
|
|
// Check link properties
|
|
expect(projectLinks[0].textContent).toBe('Project A');
|
|
expect(projectLinks[0].getAttribute('data-href')).toBe('Project A');
|
|
expect(projectLinks[0].classList.contains('internal-link')).toBe(true);
|
|
|
|
expect(projectLinks[1].textContent).toBe('Project B');
|
|
expect(projectLinks[1].getAttribute('data-href')).toBe('Project B');
|
|
expect(projectLinks[1].classList.contains('internal-link')).toBe(true);
|
|
});
|
|
|
|
it.skip('should handle project link clicks and open files', async () => {
|
|
const task = TaskFactory.createTask({
|
|
projects: ['[[Test Project]]']
|
|
});
|
|
|
|
const mockFile = new TFile('test-project.md');
|
|
mockPlugin.app.metadataCache.getFirstLinkpathDest = jest.fn().mockReturnValue(mockFile);
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
const projectLink = card.querySelector('.task-card__project-link') as HTMLElement;
|
|
|
|
expect(projectLink).toBeTruthy();
|
|
|
|
// Simulate click
|
|
const clickEvent = new MouseEvent('click', { bubbles: true });
|
|
projectLink.dispatchEvent(clickEvent);
|
|
|
|
// Wait for async operations
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
|
expect(mockPlugin.app.metadataCache.getFirstLinkpathDest).toHaveBeenCalledWith('Test Project', '');
|
|
expect(mockPlugin.app.workspace.getLeaf).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it.skip('should show notice when project link file not found', async () => {
|
|
const task = TaskFactory.createTask({
|
|
projects: ['[[Nonexistent Project]]']
|
|
});
|
|
|
|
mockPlugin.app.metadataCache.getFirstLinkpathDest = jest.fn().mockReturnValue(null);
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
const projectLink = card.querySelector('.task-card__project-link') as HTMLElement;
|
|
|
|
expect(projectLink).toBeTruthy();
|
|
|
|
// Simulate click
|
|
const clickEvent = new MouseEvent('click', { bubbles: true });
|
|
projectLink.dispatchEvent(clickEvent);
|
|
|
|
// Wait for async operations
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
|
expect(mockPlugin.app.metadataCache.getFirstLinkpathDest).toHaveBeenCalledWith('Nonexistent Project', '');
|
|
expect(Notice).toHaveBeenCalledWith('Note "Nonexistent Project" not found');
|
|
});
|
|
|
|
it('should hide metadata line when no metadata available', () => {
|
|
const task = TaskFactory.createTask({
|
|
due: undefined,
|
|
scheduled: undefined,
|
|
contexts: undefined,
|
|
timeEstimate: undefined,
|
|
timeEntries: undefined
|
|
});
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
const metadataLine = card.querySelector('.task-card__metadata') as HTMLElement;
|
|
|
|
// Should be hidden (either 'none' or empty string depending on browser)
|
|
expect(metadataLine.style.display === 'none' || metadataLine.style.display === '').toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Task Card Interactions', () => {
|
|
let task: TaskInfo;
|
|
let card: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
task = TaskFactory.createTask();
|
|
card = createTaskCard(task, mockPlugin, undefined, { showCheckbox: true });
|
|
container.appendChild(card);
|
|
});
|
|
|
|
it.skip('should handle checkbox click for regular tasks', async () => {
|
|
const checkbox = card.querySelector('.task-card__checkbox') as HTMLInputElement;
|
|
|
|
checkbox.click();
|
|
|
|
expect(mockPlugin.toggleTaskStatus).toHaveBeenCalledWith(task);
|
|
});
|
|
|
|
it.skip('should handle checkbox click for recurring tasks', async () => {
|
|
const recurringTask = TaskFactory.createRecurringTask('FREQ=DAILY');
|
|
const recurringCard = createTaskCard(recurringTask, mockPlugin, undefined, { showCheckbox: true });
|
|
const checkbox = recurringCard.querySelector('.task-card__checkbox') as HTMLInputElement;
|
|
|
|
checkbox.click();
|
|
|
|
expect(mockPlugin.toggleRecurringTaskComplete).toHaveBeenCalledWith(
|
|
recurringTask,
|
|
mockPlugin.selectedDate
|
|
);
|
|
});
|
|
|
|
it('should handle status dot click for regular tasks', async () => {
|
|
mockPlugin.cacheManager.getTaskInfo.mockResolvedValue(task);
|
|
|
|
const statusDot = card.querySelector('.task-card__status-dot') as HTMLElement;
|
|
statusDot.click();
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
|
expect(mockPlugin.cacheManager.getTaskInfo).toHaveBeenCalledWith(task.path);
|
|
expect(mockPlugin.updateTaskProperty).toHaveBeenCalledWith(task, 'status', 'done');
|
|
});
|
|
|
|
it.skip('should handle status dot click for recurring tasks', async () => {
|
|
const recurringTask = TaskFactory.createRecurringTask('FREQ=DAILY');
|
|
const recurringCard = createTaskCard(recurringTask, mockPlugin);
|
|
mockPlugin.toggleRecurringTaskComplete.mockResolvedValue(recurringTask);
|
|
|
|
const statusDot = recurringCard.querySelector('.task-card__status-dot') as HTMLElement;
|
|
statusDot.click();
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
|
expect(mockPlugin.toggleRecurringTaskComplete).toHaveBeenCalledWith(
|
|
recurringTask,
|
|
mockPlugin.selectedDate
|
|
);
|
|
});
|
|
|
|
it('should handle context menu icon click', async () => {
|
|
const contextIcon = card.querySelector('.task-card__context-menu') as HTMLElement;
|
|
const mockEvent = new MouseEvent('click', { bubbles: true });
|
|
|
|
// Mock showTaskContextMenu
|
|
const showTaskContextMenuSpy = jest.fn();
|
|
jest.doMock('../../../src/ui/TaskCard', () => ({
|
|
...jest.requireActual('../../../src/ui/TaskCard'),
|
|
showTaskContextMenu: showTaskContextMenuSpy
|
|
}));
|
|
|
|
contextIcon.dispatchEvent(mockEvent);
|
|
|
|
// Context menu should be triggered
|
|
expect(contextIcon).toBeTruthy();
|
|
});
|
|
|
|
it('should handle card click to open edit modal', async () => {
|
|
const clickEvent = new MouseEvent('click', { bubbles: true });
|
|
|
|
// Dispatch the event and wait for the handler
|
|
card.dispatchEvent(clickEvent);
|
|
|
|
// Wait for the async click handler to complete
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
expect(mockPlugin.openTaskEditModal).toHaveBeenCalledWith(task);
|
|
});
|
|
|
|
it('should not open the edit modal when clicking a Bases-rendered tag', async () => {
|
|
const basesTagsValue = {
|
|
renderTo: (el: HTMLElement) => {
|
|
el.createEl('a', {
|
|
cls: 'tag',
|
|
text: '#client',
|
|
attr: { href: '#client' },
|
|
});
|
|
},
|
|
toString: () => '#client',
|
|
};
|
|
const tagTask = TaskFactory.createTask({
|
|
customProperties: {
|
|
'file.tags': basesTagsValue,
|
|
},
|
|
});
|
|
mockPlugin.fieldMapper.lookupMappingKey = jest.fn(() => null);
|
|
const tagCard = createTaskCard(tagTask, mockPlugin, ['file.tags']);
|
|
container.appendChild(tagCard);
|
|
|
|
const tag = tagCard.querySelector('.tag') as HTMLElement;
|
|
expect(tag).toBeTruthy();
|
|
|
|
tag.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
expect(mockPlugin.openTaskEditModal).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle Ctrl+click to open source note', async () => {
|
|
const mockFile = new TFile('test.md');
|
|
mockApp.vault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
|
|
const ctrlClickEvent = new MouseEvent('click', {
|
|
bubbles: true,
|
|
ctrlKey: true
|
|
});
|
|
|
|
card.dispatchEvent(ctrlClickEvent);
|
|
|
|
// Wait for the async click handler to complete
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
expect(mockApp.vault.getAbstractFileByPath).toHaveBeenCalledWith(task.path);
|
|
expect(mockApp.workspace.openLinkText).toHaveBeenCalledWith(task.path, "", true);
|
|
});
|
|
|
|
it('should handle right-click context menu', async () => {
|
|
const contextMenuEvent = new MouseEvent('contextmenu', { bubbles: true });
|
|
|
|
card.dispatchEvent(contextMenuEvent);
|
|
|
|
// Should prevent default and trigger context menu
|
|
expect(card.dataset.taskPath).toBe(task.path);
|
|
});
|
|
|
|
it('should handle mouseover for hover preview', () => {
|
|
const mockFile = new TFile('test.md');
|
|
mockApp.vault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
|
|
const mouseoverEvent = new MouseEvent('mouseover', { bubbles: true });
|
|
card.dispatchEvent(mouseoverEvent);
|
|
|
|
expect(mockApp.workspace.trigger).toHaveBeenCalledWith('hover-link', {
|
|
event: mouseoverEvent,
|
|
source: 'tasknotes-task-card',
|
|
hoverParent: card,
|
|
targetEl: card,
|
|
linktext: task.path,
|
|
sourcePath: task.path
|
|
});
|
|
});
|
|
|
|
it.skip('should handle errors in event handlers gracefully', async () => {
|
|
mockPlugin.toggleTaskStatus.mockRejectedValue(new Error('Network error'));
|
|
|
|
const checkbox = card.querySelector('.task-card__checkbox') as HTMLInputElement;
|
|
checkbox.click();
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
|
expect(console.error).toHaveBeenCalled();
|
|
expect(Notice).toHaveBeenCalledWith(expect.stringContaining('Failed to toggle task status'));
|
|
});
|
|
});
|
|
|
|
describe('updateTaskCard', () => {
|
|
let task: TaskInfo;
|
|
let card: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
task = TaskFactory.createTask({
|
|
title: 'Original Task',
|
|
status: 'open',
|
|
priority: 'normal'
|
|
});
|
|
card = createTaskCard(task, mockPlugin, undefined, { showCheckbox: true });
|
|
});
|
|
|
|
it('should update task card with new data', () => {
|
|
const updatedTask = TaskFactory.createTask({
|
|
...task,
|
|
title: 'Updated Task',
|
|
status: 'done',
|
|
priority: 'high'
|
|
});
|
|
|
|
updateTaskCard(card, updatedTask, mockPlugin);
|
|
|
|
expect(card.classList.contains('task-card--completed')).toBe(true);
|
|
expect(card.classList.contains('task-card--priority-high')).toBe(true);
|
|
expect(card.classList.contains('task-card--status-done')).toBe(true);
|
|
|
|
const titleEl = card.querySelector('.task-card__title');
|
|
expect(titleEl?.textContent).toBe('Updated Task');
|
|
expect(titleEl?.classList.contains('completed')).toBe(true);
|
|
});
|
|
|
|
it.skip('should update checkbox state', () => {
|
|
const updatedTask = TaskFactory.createTask({
|
|
...task,
|
|
status: 'done'
|
|
});
|
|
|
|
updateTaskCard(card, updatedTask, mockPlugin);
|
|
|
|
const checkbox = card.querySelector('.task-card__checkbox') as HTMLInputElement;
|
|
expect(checkbox.checked).toBe(true);
|
|
});
|
|
|
|
it('should add priority indicator when task gains priority', () => {
|
|
// Create a task without priority initially
|
|
const taskWithoutPriority = TaskFactory.createTask({
|
|
title: 'Task without priority',
|
|
status: 'open',
|
|
priority: undefined
|
|
});
|
|
const cardWithoutPriority = createTaskCard(taskWithoutPriority, mockPlugin, undefined, { showCheckbox: true });
|
|
|
|
// Task initially has no priority indicator
|
|
expect(cardWithoutPriority.querySelector('.task-card__priority-dot')).toBeNull();
|
|
|
|
const updatedTask = TaskFactory.createTask({
|
|
...taskWithoutPriority,
|
|
priority: 'high'
|
|
});
|
|
|
|
updateTaskCard(cardWithoutPriority, updatedTask, mockPlugin);
|
|
|
|
const priorityDot = cardWithoutPriority.querySelector('.task-card__priority-dot');
|
|
expect(priorityDot).toBeTruthy();
|
|
});
|
|
|
|
it('should remove priority indicator when task loses priority', () => {
|
|
// Start with priority
|
|
const taskWithPriority = TaskFactory.createTask({
|
|
...task,
|
|
priority: 'high'
|
|
});
|
|
const cardWithPriority = createTaskCard(taskWithPriority, mockPlugin);
|
|
|
|
expect(cardWithPriority.querySelector('.task-card__priority-dot')).toBeTruthy();
|
|
|
|
const updatedTask = TaskFactory.createTask({
|
|
...taskWithPriority,
|
|
priority: undefined
|
|
});
|
|
|
|
updateTaskCard(cardWithPriority, updatedTask, mockPlugin);
|
|
|
|
expect(cardWithPriority.querySelector('.task-card__priority-dot')).toBeNull();
|
|
});
|
|
|
|
it('should update status dot color', () => {
|
|
mockPlugin.statusManager.getStatusConfig.mockReturnValue({
|
|
value: 'done',
|
|
label: 'Done',
|
|
color: '#00ff00'
|
|
});
|
|
|
|
const updatedTask = TaskFactory.createTask({
|
|
...task,
|
|
status: 'done'
|
|
});
|
|
|
|
updateTaskCard(card, updatedTask, mockPlugin);
|
|
|
|
const statusDot = card.querySelector('.task-card__status-dot') as HTMLElement;
|
|
expect(statusDot.style.borderColor).toBe('rgb(0, 255, 0)');
|
|
});
|
|
|
|
it.skip('should update metadata line', () => {
|
|
const updatedTask = TaskFactory.createTask({
|
|
...task,
|
|
due: '2025-01-15T14:30:00',
|
|
contexts: ['work']
|
|
});
|
|
|
|
updateTaskCard(card, updatedTask, mockPlugin);
|
|
|
|
const metadataLine = card.querySelector('.task-card__metadata');
|
|
expect(metadataLine?.textContent).toContain('Due:');
|
|
expect(metadataLine?.textContent).toContain('@work');
|
|
});
|
|
});
|
|
|
|
describe('showTaskContextMenu', () => {
|
|
let task: TaskInfo;
|
|
let mockMenu: any;
|
|
|
|
beforeEach(() => {
|
|
task = TaskFactory.createTask();
|
|
mockMenu = {
|
|
addItem: jest.fn((callback) => {
|
|
const mockItem = {
|
|
setTitle: jest.fn().mockReturnThis(),
|
|
setIcon: jest.fn().mockReturnThis(),
|
|
onClick: jest.fn().mockReturnThis()
|
|
};
|
|
callback(mockItem);
|
|
return mockItem;
|
|
}),
|
|
addSeparator: jest.fn(),
|
|
showAtMouseEvent: jest.fn()
|
|
};
|
|
|
|
MockObsidian.Menu.mockImplementation(() => mockMenu);
|
|
mockPlugin.cacheManager.getTaskInfo.mockResolvedValue(task);
|
|
});
|
|
|
|
it('should create context menu with basic actions', async () => {
|
|
const mockEvent = new MouseEvent('contextmenu');
|
|
|
|
await showTaskContextMenu(mockEvent, task.path, mockPlugin, new Date('2025-01-15'));
|
|
|
|
expect(mockPlugin.cacheManager.getTaskInfo).toHaveBeenCalledWith(task.path);
|
|
expect(lastMenuInstance.addItem).toHaveBeenCalled();
|
|
// Note: Menu.showAtMouseEvent might not be trackable in test environment due to Obsidian mocking complexities
|
|
// The key behavior (context menu creation and population) is verified by addItem calls
|
|
});
|
|
|
|
it('should add status options to context menu', async () => {
|
|
const mockEvent = new MouseEvent('contextmenu');
|
|
|
|
await showTaskContextMenu(mockEvent, task.path, mockPlugin, new Date('2025-01-15'));
|
|
|
|
// Should have called addItem for each status
|
|
expect(lastMenuInstance.addItem).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should add completion toggle for recurring tasks', async () => {
|
|
const recurringTask = TaskFactory.createRecurringTask('FREQ=DAILY');
|
|
mockPlugin.cacheManager.getTaskInfo.mockResolvedValue(recurringTask);
|
|
|
|
const mockEvent = new MouseEvent('contextmenu');
|
|
|
|
await showTaskContextMenu(mockEvent, recurringTask.path, mockPlugin, new Date('2025-01-15'));
|
|
|
|
expect(lastMenuInstance.addSeparator).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle task not found by showing file context menu', async () => {
|
|
mockPlugin.cacheManager.getTaskInfo.mockResolvedValue(null);
|
|
const mockFile = new TFile('nonexistent.md');
|
|
mockApp.vault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
|
|
const mockEvent = new MouseEvent('contextmenu');
|
|
|
|
await showTaskContextMenu(mockEvent, 'nonexistent.md', mockPlugin, new Date('2025-01-15'));
|
|
|
|
expect(mockApp.workspace.trigger).toHaveBeenCalledTimes(1);
|
|
const triggerArgs = mockApp.workspace.trigger.mock.calls[0];
|
|
expect(triggerArgs[0]).toBe('file-menu');
|
|
expect(triggerArgs[1]).toBeDefined();
|
|
expect(triggerArgs[2]).toBe(mockFile);
|
|
expect(triggerArgs[3]).toBe('tasknotes-bases-view');
|
|
});
|
|
|
|
it('should handle errors gracefully', async () => {
|
|
mockPlugin.cacheManager.getTaskInfo.mockRejectedValue(new Error('Cache error'));
|
|
|
|
const mockEvent = new MouseEvent('contextmenu');
|
|
|
|
await showTaskContextMenu(mockEvent, task.path, mockPlugin, new Date('2025-01-15'));
|
|
|
|
expect(console.error).toHaveBeenCalled();
|
|
expect(Notice).toHaveBeenCalledWith(expect.stringContaining('Failed to create context menu'));
|
|
});
|
|
});
|
|
|
|
describe('showDeleteConfirmationModal', () => {
|
|
let task: TaskInfo;
|
|
|
|
beforeEach(() => {
|
|
task = TaskFactory.createTask({ title: 'Task to Delete' });
|
|
});
|
|
|
|
it('should create and show delete confirmation modal', async () => {
|
|
const deletePromise = showDeleteConfirmationModal(task, mockPlugin);
|
|
|
|
// Should create promise for deletion
|
|
expect(deletePromise).toBeInstanceOf(Promise);
|
|
|
|
// Simulate user confirming deletion
|
|
mockPlugin.taskService.deleteTask.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it('should handle successful deletion', async () => {
|
|
mockPlugin.taskService.deleteTask.mockResolvedValue(undefined);
|
|
|
|
const deletePromise = showDeleteConfirmationModal(task, mockPlugin);
|
|
|
|
// Modal should be created - just verify the promise exists
|
|
expect(deletePromise).toBeInstanceOf(Promise);
|
|
});
|
|
|
|
it('should handle deletion errors', async () => {
|
|
mockPlugin.taskService.deleteTask.mockRejectedValue(new Error('Delete failed'));
|
|
|
|
const deletePromise = showDeleteConfirmationModal(task, mockPlugin);
|
|
|
|
// Modal should still be created - just verify the promise exists
|
|
expect(deletePromise).toBeInstanceOf(Promise);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases and Error Handling', () => {
|
|
it('should handle null plugin gracefully', () => {
|
|
const task = TaskFactory.createTask();
|
|
|
|
// This test should throw since the function does access plugin properties early
|
|
// The test expectation was wrong - it should throw
|
|
expect(() => createTaskCard(task, null as any, undefined, { targetDate: new Date() })).toThrow();
|
|
});
|
|
|
|
it('should handle malformed task data', () => {
|
|
const malformedTask = {
|
|
title: null,
|
|
status: undefined,
|
|
path: ''
|
|
} as any;
|
|
|
|
expect(() => createTaskCard(malformedTask, mockPlugin)).not.toThrow();
|
|
});
|
|
|
|
it('should handle missing DOM elements in update', () => {
|
|
const task = TaskFactory.createTask();
|
|
const emptyCard = document.createElement('div');
|
|
|
|
expect(() => updateTaskCard(emptyCard, task, mockPlugin)).not.toThrow();
|
|
});
|
|
|
|
it.skip('should handle network errors in async operations', async () => {
|
|
const task = TaskFactory.createTask();
|
|
const card = createTaskCard(task, mockPlugin, undefined, { showCheckbox: true });
|
|
|
|
mockPlugin.toggleTaskStatus.mockRejectedValue(new Error('Network timeout'));
|
|
|
|
const checkbox = card.querySelector('.task-card__checkbox') as HTMLInputElement;
|
|
checkbox.click();
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
|
expect(console.error).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle missing file references', async () => {
|
|
const task = TaskFactory.createTask({ path: 'nonexistent.md' });
|
|
mockApp.vault.getAbstractFileByPath.mockReturnValue(null);
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
// Simulate Ctrl+click to trigger file opening behavior
|
|
const ctrlClickEvent = new MouseEvent('click', {
|
|
bubbles: true,
|
|
ctrlKey: true
|
|
});
|
|
card.dispatchEvent(ctrlClickEvent);
|
|
|
|
// Wait for the async click handler to complete
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
expect(mockApp.vault.getAbstractFileByPath).toHaveBeenCalledWith('nonexistent.md');
|
|
});
|
|
});
|
|
|
|
describe('Performance and Memory', () => {
|
|
it('should handle large numbers of task cards efficiently', () => {
|
|
const startTime = Date.now();
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
const task = TaskFactory.createTask({ title: `Task ${i}` });
|
|
const card = createTaskCard(task, mockPlugin);
|
|
container.appendChild(card);
|
|
}
|
|
|
|
const endTime = Date.now();
|
|
expect(endTime - startTime).toBeLessThan(1000);
|
|
expect(container.children.length).toBe(100);
|
|
});
|
|
|
|
it('should clean up event listeners properly', () => {
|
|
const task = TaskFactory.createTask();
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
container.appendChild(card);
|
|
container.removeChild(card);
|
|
|
|
// Should not leak memory - hard to test directly but ensures structure is correct
|
|
expect(card.parentNode).toBeNull();
|
|
});
|
|
|
|
it('should handle rapid updates efficiently', () => {
|
|
const task = TaskFactory.createTask();
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
const startTime = Date.now();
|
|
|
|
for (let i = 0; i < 50; i++) {
|
|
const updatedTask = TaskFactory.createTask({
|
|
...task,
|
|
title: `Updated ${i}`,
|
|
status: i % 2 === 0 ? 'open' : 'done'
|
|
});
|
|
updateTaskCard(card, updatedTask, mockPlugin);
|
|
}
|
|
|
|
const endTime = Date.now();
|
|
expect(endTime - startTime).toBeLessThan(500);
|
|
});
|
|
});
|
|
|
|
|
|
describe('Subtask chevron position', () => {
|
|
it('should add task-card--chevron-left when setting is left (create)', async () => {
|
|
const task = TaskFactory.createTask({ title: 'Project Task' });
|
|
// Ensure this task is considered a project and chevron feature is on
|
|
mockPlugin.projectSubtasksService.isTaskUsedAsProject.mockResolvedValue(true);
|
|
mockPlugin.projectSubtasksService.isTaskUsedAsProjectSync.mockReturnValue(true);
|
|
mockPlugin.settings = { showExpandableSubtasks: true, subtaskChevronPosition: 'left' };
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
// Wait for async project check to complete
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
expect(card.classList.contains('task-card--chevron-left')).toBe(true);
|
|
// Chevron should render for projects when feature enabled
|
|
expect(card.querySelector('.task-card__chevron')).not.toBeNull();
|
|
});
|
|
|
|
it('should not add task-card--chevron-left when setting is right/default (create)', async () => {
|
|
const task = TaskFactory.createTask({ title: 'Project Task' });
|
|
mockPlugin.projectSubtasksService.isTaskUsedAsProject.mockResolvedValue(true);
|
|
mockPlugin.projectSubtasksService.isTaskUsedAsProjectSync.mockReturnValue(true);
|
|
mockPlugin.settings = { showExpandableSubtasks: true, subtaskChevronPosition: 'right' };
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
expect(card.classList.contains('task-card--chevron-left')).toBe(false);
|
|
expect(card.querySelector('.task-card__chevron')).not.toBeNull();
|
|
});
|
|
|
|
it('should toggle chevron-left class on update when setting changes', async () => {
|
|
const task = TaskFactory.createTask({ title: 'Project Task' });
|
|
// Start with right/default
|
|
mockPlugin.projectSubtasksService.isTaskUsedAsProject.mockResolvedValue(true);
|
|
mockPlugin.projectSubtasksService.isTaskUsedAsProjectSync.mockReturnValue(true);
|
|
mockPlugin.settings = { showExpandableSubtasks: true, subtaskChevronPosition: 'right' };
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(card.classList.contains('task-card--chevron-left')).toBe(false);
|
|
|
|
// Change to left and update
|
|
mockPlugin.settings.subtaskChevronPosition = 'left';
|
|
updateTaskCard(card, task, mockPlugin);
|
|
expect(card.classList.contains('task-card--chevron-left')).toBe(true);
|
|
|
|
// Change back to right and update
|
|
mockPlugin.settings.subtaskChevronPosition = 'right';
|
|
updateTaskCard(card, task, mockPlugin);
|
|
expect(card.classList.contains('task-card--chevron-left')).toBe(false);
|
|
});
|
|
|
|
it('should mark the subtask chevron as a no-drag control', () => {
|
|
const task = TaskFactory.createTask({ title: 'Project Task' });
|
|
mockPlugin.projectSubtasksService.isTaskUsedAsProjectSync.mockReturnValue(true);
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
const chevron = card.querySelector('.task-card__chevron') as HTMLElement;
|
|
const mouseDown = new MouseEvent('mousedown', { bubbles: true, cancelable: true });
|
|
|
|
expect(chevron.getAttribute('data-tn-no-drag')).toBe('true');
|
|
expect(chevron.getAttribute('draggable')).toBe('false');
|
|
|
|
chevron.dispatchEvent(mouseDown);
|
|
expect(mouseDown.defaultPrevented).toBe(true);
|
|
});
|
|
|
|
it('should mark the blocking toggle as a no-drag control', () => {
|
|
const task = TaskFactory.createTask({
|
|
blocking: ['tasks/dependent-a.md'],
|
|
isBlocking: true,
|
|
});
|
|
|
|
const card = createTaskCard(task, mockPlugin);
|
|
const toggle = card.querySelector('.task-card__blocking-toggle') as HTMLElement;
|
|
const mouseDown = new MouseEvent('mousedown', { bubbles: true, cancelable: true });
|
|
|
|
expect(toggle.getAttribute('data-tn-no-drag')).toBe('true');
|
|
expect(toggle.getAttribute('draggable')).toBe('false');
|
|
|
|
toggle.dispatchEvent(mouseDown);
|
|
expect(mouseDown.defaultPrevented).toBe(true);
|
|
});
|
|
});
|
|
describe('Accessibility', () => {
|
|
it('should have proper ARIA labels', () => {
|
|
const task = TaskFactory.createTask({ priority: 'high' });
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
const priorityDot = card.querySelector('.task-card__priority-dot');
|
|
expect(priorityDot?.getAttribute('aria-label')).toBe('Priority: high');
|
|
|
|
const contextIcon = card.querySelector('.task-card__context-menu');
|
|
expect(contextIcon?.getAttribute('aria-label')).toBe('Task options');
|
|
});
|
|
|
|
it.skip('should support keyboard navigation', () => {
|
|
const task = TaskFactory.createTask();
|
|
const card = createTaskCard(task, mockPlugin, undefined, { showCheckbox: true });
|
|
|
|
const checkbox = card.querySelector('.task-card__checkbox') as HTMLInputElement;
|
|
expect(checkbox.tabIndex).toBe(0);
|
|
});
|
|
|
|
it('should have proper semantic structure', () => {
|
|
const task = TaskFactory.createTask();
|
|
const card = createTaskCard(task, mockPlugin);
|
|
|
|
expect(card.querySelector('.task-card__title')).toBeTruthy();
|
|
expect(card.querySelector('.task-card__metadata')).toBeTruthy();
|
|
expect(card.querySelector('.task-card__content')).toBeTruthy();
|
|
});
|
|
});
|
|
});
|