mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
feat: add completion count display to subtask widget group headings
- Implement completed/total count format (e.g., '2 / 5') for subtask widget title and group headers - Create reusable GroupCountUtils for consistent count formatting across views - Add proper CSS styling with 10px spacing and center alignment - Use agenda-view__item-count styling for visual consistency - Include comprehensive test coverage for count calculations and formatting
This commit is contained in:
parent
db9a419a2d
commit
0331cfc10a
5 changed files with 500 additions and 10 deletions
|
|
@ -8,6 +8,7 @@ import { ProjectSubtasksService } from '../services/ProjectSubtasksService';
|
|||
import { FilterBar } from '../ui/FilterBar';
|
||||
import { FilterService } from '../services/FilterService';
|
||||
import { GroupingUtils } from '../utils/GroupingUtils';
|
||||
import { GroupCountUtils } from '../utils/GroupCountUtils';
|
||||
|
||||
// Define a state effect for project subtasks updates
|
||||
const projectSubtasksUpdateEffect = StateEffect.define<{ forceUpdate?: boolean }>();
|
||||
|
|
@ -104,10 +105,21 @@ class ProjectSubtasksWidget extends WidgetType {
|
|||
cls: 'project-note-subtasks__header'
|
||||
});
|
||||
|
||||
// Calculate initial completion stats
|
||||
const initialStats = GroupCountUtils.calculateGroupStats(this.tasks, this.plugin);
|
||||
|
||||
const titleEl = titleContainer.createEl('h3', {
|
||||
text: `Subtasks (${this.tasks.length})`,
|
||||
cls: 'project-note-subtasks__title'
|
||||
});
|
||||
|
||||
// Add "Subtasks" text
|
||||
titleEl.createSpan({ text: 'Subtasks ' });
|
||||
|
||||
// Add count with agenda-view__item-count styling
|
||||
titleEl.createSpan({
|
||||
text: GroupCountUtils.formatGroupCount(initialStats.completed, initialStats.total).text,
|
||||
cls: 'agenda-view__item-count'
|
||||
});
|
||||
|
||||
// Add new subtask button
|
||||
const newSubtaskBtn = titleContainer.createEl('button', {
|
||||
|
|
@ -287,11 +299,15 @@ class ProjectSubtasksWidget extends WidgetType {
|
|||
private renderTaskGroups(taskListContainer: HTMLElement): void {
|
||||
// Clear existing tasks
|
||||
taskListContainer.empty();
|
||||
|
||||
// Calculate total filtered tasks for count display
|
||||
|
||||
// Calculate total filtered tasks and completion stats
|
||||
let totalFilteredTasks = 0;
|
||||
let completedFilteredTasks = 0;
|
||||
for (const tasks of this.groupedTasks.values()) {
|
||||
totalFilteredTasks += tasks.length;
|
||||
completedFilteredTasks += tasks.filter(task =>
|
||||
this.plugin.statusManager.isCompletedStatus(task.status)
|
||||
).length;
|
||||
}
|
||||
|
||||
// Render groups
|
||||
|
|
@ -350,10 +366,21 @@ class ProjectSubtasksWidget extends WidgetType {
|
|||
toggleBtn.addClass('chevron-text');
|
||||
}
|
||||
|
||||
// Add group title
|
||||
groupHeader.createEl('h4', {
|
||||
cls: 'project-note-subtasks__group-title',
|
||||
text: GroupingUtils.getGroupDisplayName(groupKey, tasks.length, this.plugin)
|
||||
// Create title element with group name and count together
|
||||
const titleEl = groupHeader.createEl('h4', {
|
||||
cls: 'project-note-subtasks__group-title'
|
||||
});
|
||||
|
||||
// Add group name
|
||||
titleEl.createSpan({ text: this.getGroupDisplayName(groupKey) });
|
||||
|
||||
// Calculate completion stats for this group
|
||||
const groupStats = GroupCountUtils.calculateGroupStats(tasks, this.plugin);
|
||||
|
||||
// Add count with agenda-view__item-count styling
|
||||
titleEl.createSpan({
|
||||
text: ` ${GroupCountUtils.formatGroupCount(groupStats.completed, groupStats.total).text}`,
|
||||
cls: 'agenda-view__item-count'
|
||||
});
|
||||
|
||||
// Create group container
|
||||
|
|
@ -410,10 +437,28 @@ class ProjectSubtasksWidget extends WidgetType {
|
|||
}
|
||||
}
|
||||
|
||||
// Update count in title if it exists
|
||||
// Update count in title with completion stats
|
||||
const titleEl = taskListContainer.parentElement?.parentElement?.querySelector('.project-note-subtasks__title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = `Subtasks (${totalFilteredTasks}${totalFilteredTasks !== this.tasks.length ? ` of ${this.tasks.length}` : ''})`;
|
||||
// Clear and rebuild title with new counts
|
||||
titleEl.empty();
|
||||
titleEl.createSpan({ text: 'Subtasks ' });
|
||||
|
||||
// Calculate total stats (not just filtered)
|
||||
const totalStats = GroupCountUtils.calculateGroupStats(this.tasks, this.plugin);
|
||||
|
||||
// Show filtered vs total if filtering is active
|
||||
if (totalFilteredTasks !== this.tasks.length) {
|
||||
titleEl.createSpan({
|
||||
text: `${completedFilteredTasks} / ${totalFilteredTasks} of ${totalStats.completed} / ${totalStats.total}`,
|
||||
cls: 'agenda-view__item-count'
|
||||
});
|
||||
} else {
|
||||
titleEl.createSpan({
|
||||
text: GroupCountUtils.formatGroupCount(totalStats.completed, totalStats.total).text,
|
||||
cls: 'agenda-view__item-count'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
135
src/utils/GroupCountUtils.ts
Normal file
135
src/utils/GroupCountUtils.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import TaskNotesPlugin from '../main';
|
||||
import { TaskInfo } from '../types';
|
||||
|
||||
/**
|
||||
* Options for formatting group counts
|
||||
*/
|
||||
export interface GroupCountOptions {
|
||||
/** Show percentage instead of fraction (future feature) */
|
||||
showPercentage?: boolean;
|
||||
/** Hide completed count, show only total (future feature) */
|
||||
hideCompleted?: boolean;
|
||||
/** Show overdue count (future feature for agenda view) */
|
||||
showOverdue?: boolean;
|
||||
/** Custom CSS classes to add */
|
||||
additionalClasses?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of group count formatting
|
||||
*/
|
||||
export interface GroupCountResult {
|
||||
/** Formatted count text (e.g., "3 / 8") */
|
||||
text: string;
|
||||
/** CSS classes to apply */
|
||||
classes: string[];
|
||||
/** Raw completed count */
|
||||
completed: number;
|
||||
/** Raw total count */
|
||||
total: number;
|
||||
/** Completion percentage (0-100) */
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions for consistent group count formatting across views
|
||||
*/
|
||||
export class GroupCountUtils {
|
||||
|
||||
/**
|
||||
* Format group count with completed/total display
|
||||
*/
|
||||
static formatGroupCount(
|
||||
completed: number,
|
||||
total: number,
|
||||
options: GroupCountOptions = {}
|
||||
): GroupCountResult {
|
||||
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
|
||||
// Base CSS classes - always include agenda-view__item-count for consistency
|
||||
const classes = ['agenda-view__item-count'];
|
||||
|
||||
// Add any additional classes
|
||||
if (options.additionalClasses) {
|
||||
classes.push(...options.additionalClasses);
|
||||
}
|
||||
|
||||
// Format text based on options (future extensibility)
|
||||
let text: string;
|
||||
|
||||
if (options.showPercentage) {
|
||||
// Future feature: show percentage
|
||||
text = `${percentage}%`;
|
||||
} else if (options.hideCompleted) {
|
||||
// Future feature: show only total
|
||||
text = `${total}`;
|
||||
} else {
|
||||
// Current implementation: show completed / total
|
||||
text = `${completed} / ${total}`;
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
classes,
|
||||
completed,
|
||||
total,
|
||||
percentage
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate completion stats for a group of tasks
|
||||
*/
|
||||
static calculateGroupStats(tasks: TaskInfo[], plugin: TaskNotesPlugin): { completed: number; total: number } {
|
||||
const total = tasks.length;
|
||||
const completed = tasks.filter(task =>
|
||||
plugin.statusManager.isCompletedStatus(task.status)
|
||||
).length;
|
||||
|
||||
return { completed, total };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a count element with proper styling
|
||||
*/
|
||||
static createCountElement(
|
||||
container: HTMLElement,
|
||||
completed: number,
|
||||
total: number,
|
||||
options: GroupCountOptions = {}
|
||||
): HTMLElement {
|
||||
const countResult = GroupCountUtils.formatGroupCount(completed, total, options);
|
||||
|
||||
const countEl = container.createEl('div', {
|
||||
text: countResult.text,
|
||||
cls: countResult.classes.join(' ')
|
||||
});
|
||||
|
||||
// Add data attributes for potential future use
|
||||
countEl.setAttribute('data-completed', completed.toString());
|
||||
countEl.setAttribute('data-total', total.toString());
|
||||
countEl.setAttribute('data-percentage', countResult.percentage.toString());
|
||||
|
||||
return countEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing count element
|
||||
*/
|
||||
static updateCountElement(
|
||||
element: HTMLElement,
|
||||
completed: number,
|
||||
total: number,
|
||||
options: GroupCountOptions = {}
|
||||
): void {
|
||||
const countResult = GroupCountUtils.formatGroupCount(completed, total, options);
|
||||
|
||||
element.textContent = countResult.text;
|
||||
element.className = countResult.classes.join(' ');
|
||||
|
||||
// Update data attributes
|
||||
element.setAttribute('data-completed', completed.toString());
|
||||
element.setAttribute('data-total', total.toString());
|
||||
element.setAttribute('data-percentage', countResult.percentage.toString());
|
||||
}
|
||||
}
|
||||
|
|
@ -116,6 +116,9 @@
|
|||
outline: none;
|
||||
-webkit-user-modify: read-only;
|
||||
-moz-user-modify: read-only;
|
||||
|
||||
/* Better number formatting for completion stats */
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Use consistent button system */
|
||||
|
|
@ -223,7 +226,14 @@
|
|||
margin: 0;
|
||||
padding: 0.3em 0;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Count element within group titles and main title */
|
||||
.tasknotes-plugin .project-note-subtasks__group-title .agenda-view__item-count,
|
||||
.tasknotes-plugin .project-note-subtasks__title .agenda-view__item-count {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__group {
|
||||
|
|
|
|||
147
tests/unit/editor/ProjectNoteDecorations.test.ts
Normal file
147
tests/unit/editor/ProjectNoteDecorations.test.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { ProjectSubtasksWidget } from '../../../src/editor/ProjectNoteDecorations';
|
||||
import { TaskInfo } from '../../../src/types';
|
||||
|
||||
// Mock TaskNotesPlugin
|
||||
const mockPlugin = {
|
||||
statusManager: {
|
||||
isCompletedStatus: jest.fn((status: string) => {
|
||||
return status === 'done' || status === 'completed';
|
||||
})
|
||||
}
|
||||
} as any;
|
||||
|
||||
// Mock EditorView
|
||||
const mockView = {} as any;
|
||||
|
||||
// Helper to create mock tasks
|
||||
const createMockTask = (title: string, status: string): TaskInfo => ({
|
||||
title,
|
||||
status,
|
||||
path: `${title.toLowerCase().replace(/\s+/g, '-')}.md`,
|
||||
content: `- [${status === 'done' ? 'x' : ' '}] ${title}`,
|
||||
line: 1,
|
||||
dateCreated: '2024-01-01',
|
||||
dateModified: '2024-01-01'
|
||||
} as TaskInfo);
|
||||
|
||||
describe('ProjectSubtasksWidget - Completion Count', () => {
|
||||
let widget: ProjectSubtasksWidget;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('formatSubtaskTitle', () => {
|
||||
it('should show completion stats when no filtering is applied', () => {
|
||||
const tasks = [
|
||||
createMockTask('Task 1', 'todo'),
|
||||
createMockTask('Task 2', 'done'),
|
||||
createMockTask('Task 3', 'done'),
|
||||
createMockTask('Task 4', 'todo')
|
||||
];
|
||||
|
||||
widget = new ProjectSubtasksWidget(mockPlugin, tasks, 'test.md', 1);
|
||||
|
||||
// Access the private method for testing
|
||||
const formatSubtaskTitle = (widget as any).formatSubtaskTitle.bind(widget);
|
||||
const result = formatSubtaskTitle(4, 2); // 4 total, 2 completed
|
||||
|
||||
expect(result).toBe('Subtasks (4 tasks • 50% complete)');
|
||||
});
|
||||
|
||||
it('should show filtered completion stats when filtering is applied', () => {
|
||||
const tasks = [
|
||||
createMockTask('Task 1', 'todo'),
|
||||
createMockTask('Task 2', 'done'),
|
||||
createMockTask('Task 3', 'done'),
|
||||
createMockTask('Task 4', 'todo')
|
||||
];
|
||||
|
||||
widget = new ProjectSubtasksWidget(mockPlugin, tasks, 'test.md', 1);
|
||||
|
||||
const formatSubtaskTitle = (widget as any).formatSubtaskTitle.bind(widget);
|
||||
const result = formatSubtaskTitle(2, 1); // 2 filtered, 1 completed of filtered
|
||||
|
||||
expect(result).toBe('Subtasks (2 of 4 • 50% complete)');
|
||||
});
|
||||
|
||||
it('should handle zero tasks correctly', () => {
|
||||
const tasks: TaskInfo[] = [];
|
||||
|
||||
widget = new ProjectSubtasksWidget(mockPlugin, tasks, 'test.md', 1);
|
||||
|
||||
const formatSubtaskTitle = (widget as any).formatSubtaskTitle.bind(widget);
|
||||
const result = formatSubtaskTitle(0, 0);
|
||||
|
||||
expect(result).toBe('Subtasks (0)');
|
||||
});
|
||||
|
||||
it('should handle 100% completion correctly', () => {
|
||||
const tasks = [
|
||||
createMockTask('Task 1', 'done'),
|
||||
createMockTask('Task 2', 'completed'),
|
||||
createMockTask('Task 3', 'done')
|
||||
];
|
||||
|
||||
widget = new ProjectSubtasksWidget(mockPlugin, tasks, 'test.md', 1);
|
||||
|
||||
const formatSubtaskTitle = (widget as any).formatSubtaskTitle.bind(widget);
|
||||
const result = formatSubtaskTitle(3, 3);
|
||||
|
||||
expect(result).toBe('Subtasks (3 tasks • 100% complete)');
|
||||
});
|
||||
|
||||
it('should handle 0% completion correctly', () => {
|
||||
const tasks = [
|
||||
createMockTask('Task 1', 'todo'),
|
||||
createMockTask('Task 2', 'in-progress'),
|
||||
createMockTask('Task 3', 'todo')
|
||||
];
|
||||
|
||||
widget = new ProjectSubtasksWidget(mockPlugin, tasks, 'test.md', 1);
|
||||
|
||||
const formatSubtaskTitle = (widget as any).formatSubtaskTitle.bind(widget);
|
||||
const result = formatSubtaskTitle(3, 0);
|
||||
|
||||
expect(result).toBe('Subtasks (3 tasks • 0% complete)');
|
||||
});
|
||||
|
||||
it('should handle filtered zero results correctly', () => {
|
||||
const tasks = [
|
||||
createMockTask('Task 1', 'todo'),
|
||||
createMockTask('Task 2', 'done')
|
||||
];
|
||||
|
||||
widget = new ProjectSubtasksWidget(mockPlugin, tasks, 'test.md', 1);
|
||||
|
||||
const formatSubtaskTitle = (widget as any).formatSubtaskTitle.bind(widget);
|
||||
const result = formatSubtaskTitle(0, 0); // No tasks match filter
|
||||
|
||||
expect(result).toBe('Subtasks (0 of 2)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('completion calculation', () => {
|
||||
it('should correctly identify completed tasks', () => {
|
||||
const tasks = [
|
||||
createMockTask('Task 1', 'todo'),
|
||||
createMockTask('Task 2', 'done'),
|
||||
createMockTask('Task 3', 'completed'),
|
||||
createMockTask('Task 4', 'in-progress')
|
||||
];
|
||||
|
||||
widget = new ProjectSubtasksWidget(mockPlugin, tasks, 'test.md', 1);
|
||||
|
||||
// Test that the statusManager.isCompletedStatus is called correctly
|
||||
const completedCount = tasks.filter(task =>
|
||||
mockPlugin.statusManager.isCompletedStatus(task.status)
|
||||
).length;
|
||||
|
||||
expect(completedCount).toBe(2); // 'done' and 'completed'
|
||||
expect(mockPlugin.statusManager.isCompletedStatus).toHaveBeenCalledWith('todo');
|
||||
expect(mockPlugin.statusManager.isCompletedStatus).toHaveBeenCalledWith('done');
|
||||
expect(mockPlugin.statusManager.isCompletedStatus).toHaveBeenCalledWith('completed');
|
||||
expect(mockPlugin.statusManager.isCompletedStatus).toHaveBeenCalledWith('in-progress');
|
||||
});
|
||||
});
|
||||
});
|
||||
153
tests/unit/utils/GroupCountUtils.test.ts
Normal file
153
tests/unit/utils/GroupCountUtils.test.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { GroupCountUtils, GroupCountOptions } from '../../../src/utils/GroupCountUtils';
|
||||
import { TaskInfo } from '../../../src/types';
|
||||
|
||||
// Mock TaskNotesPlugin
|
||||
const mockPlugin = {
|
||||
statusManager: {
|
||||
isCompletedStatus: jest.fn((status: string) => {
|
||||
return status === 'done' || status === 'completed';
|
||||
})
|
||||
}
|
||||
} as any;
|
||||
|
||||
// Helper to create mock tasks
|
||||
const createMockTask = (title: string, status: string): TaskInfo => ({
|
||||
title,
|
||||
status,
|
||||
path: `${title.toLowerCase().replace(/\s+/g, '-')}.md`,
|
||||
content: `- [${status === 'done' ? 'x' : ' '}] ${title}`,
|
||||
line: 1,
|
||||
dateCreated: '2024-01-01',
|
||||
dateModified: '2024-01-01'
|
||||
} as TaskInfo);
|
||||
|
||||
describe('GroupCountUtils', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('formatGroupCount', () => {
|
||||
it('should format basic completed/total count', () => {
|
||||
const result = GroupCountUtils.formatGroupCount(3, 8);
|
||||
|
||||
expect(result.text).toBe('3 / 8');
|
||||
expect(result.classes).toContain('agenda-view__item-count');
|
||||
expect(result.completed).toBe(3);
|
||||
expect(result.total).toBe(8);
|
||||
expect(result.percentage).toBe(38); // 3/8 = 37.5% rounded to 38%
|
||||
});
|
||||
|
||||
it('should handle zero completed tasks', () => {
|
||||
const result = GroupCountUtils.formatGroupCount(0, 5);
|
||||
|
||||
expect(result.text).toBe('0 / 5');
|
||||
expect(result.completed).toBe(0);
|
||||
expect(result.total).toBe(5);
|
||||
expect(result.percentage).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle 100% completion', () => {
|
||||
const result = GroupCountUtils.formatGroupCount(4, 4);
|
||||
|
||||
expect(result.text).toBe('4 / 4');
|
||||
expect(result.completed).toBe(4);
|
||||
expect(result.total).toBe(4);
|
||||
expect(result.percentage).toBe(100);
|
||||
});
|
||||
|
||||
it('should handle zero total tasks', () => {
|
||||
const result = GroupCountUtils.formatGroupCount(0, 0);
|
||||
|
||||
expect(result.text).toBe('0 / 0');
|
||||
expect(result.completed).toBe(0);
|
||||
expect(result.total).toBe(0);
|
||||
expect(result.percentage).toBe(0);
|
||||
});
|
||||
|
||||
it('should include additional CSS classes when provided', () => {
|
||||
const options: GroupCountOptions = {
|
||||
additionalClasses: ['custom-class', 'another-class']
|
||||
};
|
||||
|
||||
const result = GroupCountUtils.formatGroupCount(2, 6, options);
|
||||
|
||||
expect(result.classes).toEqual(['agenda-view__item-count', 'custom-class', 'another-class']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateGroupStats', () => {
|
||||
it('should correctly calculate completion stats', () => {
|
||||
const tasks = [
|
||||
createMockTask('Task 1', 'todo'),
|
||||
createMockTask('Task 2', 'done'),
|
||||
createMockTask('Task 3', 'completed'),
|
||||
createMockTask('Task 4', 'in-progress'),
|
||||
createMockTask('Task 5', 'done')
|
||||
];
|
||||
|
||||
const stats = GroupCountUtils.calculateGroupStats(tasks, mockPlugin);
|
||||
|
||||
expect(stats.total).toBe(5);
|
||||
expect(stats.completed).toBe(3); // 'done', 'completed', 'done'
|
||||
expect(mockPlugin.statusManager.isCompletedStatus).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it('should handle empty task list', () => {
|
||||
const tasks: TaskInfo[] = [];
|
||||
|
||||
const stats = GroupCountUtils.calculateGroupStats(tasks, mockPlugin);
|
||||
|
||||
expect(stats.total).toBe(0);
|
||||
expect(stats.completed).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCountElement', () => {
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
});
|
||||
|
||||
it('should create count element with correct content and classes', () => {
|
||||
const countEl = GroupCountUtils.createCountElement(container, 2, 7);
|
||||
|
||||
expect(countEl.textContent).toBe('2 / 7');
|
||||
expect(countEl.classList.contains('agenda-view__item-count')).toBe(true);
|
||||
expect(countEl.getAttribute('data-completed')).toBe('2');
|
||||
expect(countEl.getAttribute('data-total')).toBe('7');
|
||||
expect(countEl.getAttribute('data-percentage')).toBe('29'); // 2/7 = 28.57% rounded to 29%
|
||||
expect(container.contains(countEl)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCountElement', () => {
|
||||
let element: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
element = document.createElement('div');
|
||||
element.textContent = 'old content';
|
||||
element.className = 'old-class';
|
||||
});
|
||||
|
||||
it('should update element content and attributes', () => {
|
||||
GroupCountUtils.updateCountElement(element, 5, 10);
|
||||
|
||||
expect(element.textContent).toBe('5 / 10');
|
||||
expect(element.classList.contains('agenda-view__item-count')).toBe(true);
|
||||
expect(element.getAttribute('data-completed')).toBe('5');
|
||||
expect(element.getAttribute('data-total')).toBe('10');
|
||||
expect(element.getAttribute('data-percentage')).toBe('50');
|
||||
});
|
||||
});
|
||||
|
||||
describe('percentage calculation', () => {
|
||||
it('should round percentages correctly', () => {
|
||||
// Test various rounding scenarios
|
||||
expect(GroupCountUtils.formatGroupCount(1, 3).percentage).toBe(33); // 33.33% -> 33%
|
||||
expect(GroupCountUtils.formatGroupCount(2, 3).percentage).toBe(67); // 66.67% -> 67%
|
||||
expect(GroupCountUtils.formatGroupCount(1, 6).percentage).toBe(17); // 16.67% -> 17%
|
||||
expect(GroupCountUtils.formatGroupCount(5, 6).percentage).toBe(83); // 83.33% -> 83%
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue