@tuturuuu/utils 0.0.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +305 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +122 -3
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- package/src/index.ts +0 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const { mockCreateWorkspaceTask } = vi.hoisted(() => ({
|
|
5
|
+
mockCreateWorkspaceTask: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('@tuturuuu/supabase/next/client', () => ({
|
|
9
|
+
createClient: vi.fn(() => ({})),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@tuturuuu/internal-api/tasks', async () => {
|
|
13
|
+
const actual = await vi.importActual<
|
|
14
|
+
typeof import('@tuturuuu/internal-api/tasks')
|
|
15
|
+
>('@tuturuuu/internal-api/tasks');
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
createWorkspaceTask: mockCreateWorkspaceTask,
|
|
20
|
+
triggerWorkspaceTaskEmbedding: vi.fn().mockResolvedValue({ success: true }),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
import { createTask } from '../task-helper';
|
|
25
|
+
|
|
26
|
+
type TaskCreateInputWithScheduling = Partial<Task> & {
|
|
27
|
+
total_duration?: number | null;
|
|
28
|
+
is_splittable?: boolean | null;
|
|
29
|
+
min_split_duration_minutes?: number | null;
|
|
30
|
+
max_split_duration_minutes?: number | null;
|
|
31
|
+
calendar_hours?: Task['calendar_hours'];
|
|
32
|
+
auto_schedule?: boolean | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('createTask scheduling persistence', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
mockCreateWorkspaceTask.mockResolvedValue({
|
|
39
|
+
task: {
|
|
40
|
+
id: 'task-1',
|
|
41
|
+
name: 'Task title',
|
|
42
|
+
list_id: 'list-1',
|
|
43
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('sends null scheduling fields when none are provided', async () => {
|
|
49
|
+
await createTask('ws-1', 'list-1', {
|
|
50
|
+
name: 'Task title',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(mockCreateWorkspaceTask).toHaveBeenCalledTimes(1);
|
|
54
|
+
expect(mockCreateWorkspaceTask).toHaveBeenCalledWith(
|
|
55
|
+
'ws-1',
|
|
56
|
+
expect.objectContaining({
|
|
57
|
+
name: 'Task title',
|
|
58
|
+
listId: 'list-1',
|
|
59
|
+
}),
|
|
60
|
+
expect.any(Object)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const payload = mockCreateWorkspaceTask.mock.calls[0]?.[1] as Record<
|
|
64
|
+
string,
|
|
65
|
+
unknown
|
|
66
|
+
>;
|
|
67
|
+
|
|
68
|
+
expect(payload.total_duration).toBeNull();
|
|
69
|
+
expect(payload.is_splittable).toBeNull();
|
|
70
|
+
expect(payload.min_split_duration_minutes).toBeNull();
|
|
71
|
+
expect(payload.max_split_duration_minutes).toBeNull();
|
|
72
|
+
expect(payload.calendar_hours).toBeNull();
|
|
73
|
+
expect(payload.auto_schedule).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('forwards scheduling fields when they are provided', async () => {
|
|
77
|
+
const taskInput: TaskCreateInputWithScheduling = {
|
|
78
|
+
name: 'Task title',
|
|
79
|
+
total_duration: 2,
|
|
80
|
+
is_splittable: true,
|
|
81
|
+
min_split_duration_minutes: 30,
|
|
82
|
+
max_split_duration_minutes: 120,
|
|
83
|
+
calendar_hours: 'work_hours',
|
|
84
|
+
auto_schedule: true,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
await createTask('ws-1', 'list-1', taskInput);
|
|
88
|
+
|
|
89
|
+
expect(mockCreateWorkspaceTask).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(mockCreateWorkspaceTask).toHaveBeenCalledWith(
|
|
91
|
+
'ws-1',
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
total_duration: 2,
|
|
94
|
+
is_splittable: true,
|
|
95
|
+
min_split_duration_minutes: 30,
|
|
96
|
+
max_split_duration_minutes: 120,
|
|
97
|
+
calendar_hours: 'work_hours',
|
|
98
|
+
auto_schedule: true,
|
|
99
|
+
}),
|
|
100
|
+
expect.any(Object)
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('keeps explicit false/null scheduling values instead of forcing defaults', async () => {
|
|
105
|
+
const taskInput: TaskCreateInputWithScheduling = {
|
|
106
|
+
name: 'Task title',
|
|
107
|
+
total_duration: null,
|
|
108
|
+
is_splittable: false,
|
|
109
|
+
min_split_duration_minutes: null,
|
|
110
|
+
max_split_duration_minutes: null,
|
|
111
|
+
calendar_hours: null,
|
|
112
|
+
auto_schedule: false,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
await createTask('ws-1', 'list-1', taskInput);
|
|
116
|
+
|
|
117
|
+
expect(mockCreateWorkspaceTask).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(mockCreateWorkspaceTask).toHaveBeenCalledWith(
|
|
119
|
+
'ws-1',
|
|
120
|
+
expect.objectContaining({
|
|
121
|
+
is_splittable: false,
|
|
122
|
+
auto_schedule: false,
|
|
123
|
+
total_duration: null,
|
|
124
|
+
calendar_hours: null,
|
|
125
|
+
}),
|
|
126
|
+
expect.any(Object)
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
calculateOverdueDays,
|
|
4
|
+
filterTasks,
|
|
5
|
+
getFilteredMetrics,
|
|
6
|
+
getStatusColor,
|
|
7
|
+
getTaskCompletionDate,
|
|
8
|
+
groupTasksByStatus,
|
|
9
|
+
hasDraggableData,
|
|
10
|
+
} from '../task-helpers';
|
|
11
|
+
|
|
12
|
+
describe('Task Helpers', () => {
|
|
13
|
+
describe('hasDraggableData', () => {
|
|
14
|
+
it('returns false for null element', () => {
|
|
15
|
+
expect(hasDraggableData(null)).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns false for element without data', () => {
|
|
19
|
+
expect(hasDraggableData({ id: '1' } as any)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns false for element without current data', () => {
|
|
23
|
+
expect(hasDraggableData({ id: '1', data: {} } as any)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns true for valid Task type with task data', () => {
|
|
27
|
+
const element = {
|
|
28
|
+
id: '1',
|
|
29
|
+
data: { current: { type: 'Task', task: { id: 'task-1' } } },
|
|
30
|
+
};
|
|
31
|
+
expect(hasDraggableData(element as any)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns true for valid Column type with column data', () => {
|
|
35
|
+
const element = {
|
|
36
|
+
id: '1',
|
|
37
|
+
data: { current: { type: 'Column', column: { id: 'col-1' } } },
|
|
38
|
+
};
|
|
39
|
+
expect(hasDraggableData(element as any)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns false for Task type without task data', () => {
|
|
43
|
+
const element = {
|
|
44
|
+
id: '1',
|
|
45
|
+
data: { current: { type: 'Task' } },
|
|
46
|
+
};
|
|
47
|
+
expect(hasDraggableData(element as any)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns false for Column type without column data', () => {
|
|
51
|
+
const element = {
|
|
52
|
+
id: '1',
|
|
53
|
+
data: { current: { type: 'Column' } },
|
|
54
|
+
};
|
|
55
|
+
expect(hasDraggableData(element as any)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns false for unknown type', () => {
|
|
59
|
+
const element = {
|
|
60
|
+
id: '1',
|
|
61
|
+
data: { current: { type: 'Unknown' } },
|
|
62
|
+
};
|
|
63
|
+
expect(hasDraggableData(element as any)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('calculateOverdueDays', () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
vi.useFakeTimers();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
vi.useRealTimers();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns positive days for past due date', () => {
|
|
77
|
+
vi.setSystemTime(new Date('2024-01-15T00:00:00'));
|
|
78
|
+
// Due date was Jan 10 midnight, current is Jan 15 midnight = 5 days
|
|
79
|
+
expect(calculateOverdueDays('2024-01-10T00:00:00')).toBe(5);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns zero for today due date', () => {
|
|
83
|
+
vi.setSystemTime(new Date('2024-01-15T12:00:00'));
|
|
84
|
+
expect(calculateOverdueDays('2024-01-15')).toBe(1); // Less than 24h from midnight
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns negative days for future due date', () => {
|
|
88
|
+
vi.setSystemTime(new Date('2024-01-15T00:00:00'));
|
|
89
|
+
// Due date is Jan 20 midnight, current is Jan 15 midnight = -5 days
|
|
90
|
+
expect(calculateOverdueDays('2024-01-20T00:00:00')).toBe(-5);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('handles Date objects', () => {
|
|
94
|
+
vi.setSystemTime(new Date('2024-01-15T00:00:00'));
|
|
95
|
+
expect(calculateOverdueDays(new Date('2024-01-10T00:00:00'))).toBe(5);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('handles ISO date strings', () => {
|
|
99
|
+
vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
|
|
100
|
+
expect(calculateOverdueDays('2024-01-10T00:00:00Z')).toBeGreaterThan(0);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('getTaskCompletionDate', () => {
|
|
105
|
+
it('returns completed_at date when available', () => {
|
|
106
|
+
const task = {
|
|
107
|
+
id: '1',
|
|
108
|
+
name: 'Test',
|
|
109
|
+
boardId: 'b1',
|
|
110
|
+
boardName: 'Board',
|
|
111
|
+
listName: 'List',
|
|
112
|
+
completed_at: '2024-01-15T10:00:00Z',
|
|
113
|
+
};
|
|
114
|
+
const result = getTaskCompletionDate(task);
|
|
115
|
+
expect(result).toEqual(new Date('2024-01-15T10:00:00Z'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns updated_at when completed_at is not available', () => {
|
|
119
|
+
const task = {
|
|
120
|
+
id: '1',
|
|
121
|
+
name: 'Test',
|
|
122
|
+
boardId: 'b1',
|
|
123
|
+
boardName: 'Board',
|
|
124
|
+
listName: 'List',
|
|
125
|
+
updated_at: '2024-01-14T10:00:00Z',
|
|
126
|
+
};
|
|
127
|
+
const result = getTaskCompletionDate(task);
|
|
128
|
+
expect(result).toEqual(new Date('2024-01-14T10:00:00Z'));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('checks multiple completion date fields in order', () => {
|
|
132
|
+
const task = {
|
|
133
|
+
id: '1',
|
|
134
|
+
name: 'Test',
|
|
135
|
+
boardId: 'b1',
|
|
136
|
+
boardName: 'Board',
|
|
137
|
+
listName: 'List',
|
|
138
|
+
closed_at: '2024-01-13T10:00:00Z',
|
|
139
|
+
finished_at: '2024-01-12T10:00:00Z',
|
|
140
|
+
};
|
|
141
|
+
const result = getTaskCompletionDate(task);
|
|
142
|
+
expect(result).toEqual(new Date('2024-01-13T10:00:00Z'));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns null for task without completion dates', () => {
|
|
146
|
+
const task = {
|
|
147
|
+
id: '1',
|
|
148
|
+
name: 'Test',
|
|
149
|
+
boardId: 'b1',
|
|
150
|
+
boardName: 'Board',
|
|
151
|
+
listName: 'List',
|
|
152
|
+
};
|
|
153
|
+
const result = getTaskCompletionDate(task);
|
|
154
|
+
expect(result).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('falls back to updated_at for done status tasks', () => {
|
|
158
|
+
const task = {
|
|
159
|
+
id: '1',
|
|
160
|
+
name: 'Test',
|
|
161
|
+
boardId: 'b1',
|
|
162
|
+
boardName: 'Board',
|
|
163
|
+
listName: 'List',
|
|
164
|
+
listStatus: 'done',
|
|
165
|
+
updated_at: '2024-01-15T10:00:00Z',
|
|
166
|
+
};
|
|
167
|
+
const result = getTaskCompletionDate(task);
|
|
168
|
+
expect(result).toEqual(new Date('2024-01-15T10:00:00Z'));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('falls back to created_at for archived tasks without updated_at', () => {
|
|
172
|
+
const task = {
|
|
173
|
+
id: '1',
|
|
174
|
+
name: 'Test',
|
|
175
|
+
boardId: 'b1',
|
|
176
|
+
boardName: 'Board',
|
|
177
|
+
listName: 'List',
|
|
178
|
+
archived: true,
|
|
179
|
+
created_at: '2024-01-10T10:00:00Z',
|
|
180
|
+
};
|
|
181
|
+
const result = getTaskCompletionDate(task);
|
|
182
|
+
expect(result).toEqual(new Date('2024-01-10T10:00:00Z'));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('handles invalid date strings gracefully', () => {
|
|
186
|
+
const task = {
|
|
187
|
+
id: '1',
|
|
188
|
+
name: 'Test',
|
|
189
|
+
boardId: 'b1',
|
|
190
|
+
boardName: 'Board',
|
|
191
|
+
listName: 'List',
|
|
192
|
+
completed_at: 'invalid-date',
|
|
193
|
+
updated_at: '2024-01-15T10:00:00Z',
|
|
194
|
+
};
|
|
195
|
+
const result = getTaskCompletionDate(task);
|
|
196
|
+
expect(result).toEqual(new Date('2024-01-15T10:00:00Z'));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('getStatusColor', () => {
|
|
201
|
+
it('returns green for done status', () => {
|
|
202
|
+
expect(getStatusColor('done')).toBe('bg-green-500');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('returns green for closed status', () => {
|
|
206
|
+
expect(getStatusColor('closed')).toBe('bg-green-500');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns blue for active status', () => {
|
|
210
|
+
expect(getStatusColor('active')).toBe('bg-blue-500');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('returns gray for unknown status', () => {
|
|
214
|
+
expect(getStatusColor('unknown')).toBe('bg-gray-400');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('returns gray for empty status', () => {
|
|
218
|
+
expect(getStatusColor('')).toBe('bg-gray-400');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('returns gray for not_started status', () => {
|
|
222
|
+
expect(getStatusColor('not_started')).toBe('bg-gray-400');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('filterTasks', () => {
|
|
227
|
+
const tasks = [
|
|
228
|
+
{
|
|
229
|
+
id: '1',
|
|
230
|
+
name: 'Task 1',
|
|
231
|
+
boardId: 'board-1',
|
|
232
|
+
boardName: 'Board 1',
|
|
233
|
+
listName: 'To Do',
|
|
234
|
+
listStatus: 'not_started',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: '2',
|
|
238
|
+
name: 'Task 2',
|
|
239
|
+
boardId: 'board-1',
|
|
240
|
+
boardName: 'Board 1',
|
|
241
|
+
listName: 'In Progress',
|
|
242
|
+
listStatus: 'active',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: '3',
|
|
246
|
+
name: 'Task 3',
|
|
247
|
+
boardId: 'board-2',
|
|
248
|
+
boardName: 'Board 2',
|
|
249
|
+
listName: 'Done',
|
|
250
|
+
listStatus: 'done',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: '4',
|
|
254
|
+
name: 'Task 4',
|
|
255
|
+
boardId: 'board-2',
|
|
256
|
+
boardName: 'Board 2',
|
|
257
|
+
listName: 'Closed',
|
|
258
|
+
listStatus: 'closed',
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
it('returns all tasks when no filters applied', () => {
|
|
263
|
+
const result = filterTasks(tasks, null, 'all');
|
|
264
|
+
expect(result).toHaveLength(4);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('filters by board ID', () => {
|
|
268
|
+
const result = filterTasks(tasks, 'board-1', 'all');
|
|
269
|
+
expect(result).toHaveLength(2);
|
|
270
|
+
expect(result.every((t) => t.boardId === 'board-1')).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('filters by status not_started', () => {
|
|
274
|
+
const result = filterTasks(tasks, null, 'not_started');
|
|
275
|
+
expect(result).toHaveLength(1);
|
|
276
|
+
expect(result[0]?.id).toBe('1');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('filters by status active', () => {
|
|
280
|
+
const result = filterTasks(tasks, null, 'active');
|
|
281
|
+
expect(result).toHaveLength(1);
|
|
282
|
+
expect(result[0]?.id).toBe('2');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('filters by status done', () => {
|
|
286
|
+
const result = filterTasks(tasks, null, 'done');
|
|
287
|
+
expect(result).toHaveLength(1);
|
|
288
|
+
expect(result[0]?.id).toBe('3');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('filters by status closed', () => {
|
|
292
|
+
const result = filterTasks(tasks, null, 'closed');
|
|
293
|
+
expect(result).toHaveLength(1);
|
|
294
|
+
expect(result[0]?.id).toBe('4');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('combines board and status filters', () => {
|
|
298
|
+
const result = filterTasks(tasks, 'board-1', 'active');
|
|
299
|
+
expect(result).toHaveLength(1);
|
|
300
|
+
expect(result[0]?.id).toBe('2');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('returns empty array when no matches', () => {
|
|
304
|
+
const result = filterTasks(tasks, 'board-1', 'done');
|
|
305
|
+
expect(result).toHaveLength(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('handles tasks without listStatus as not_started', () => {
|
|
309
|
+
const tasksWithoutStatus = [
|
|
310
|
+
{
|
|
311
|
+
id: '1',
|
|
312
|
+
name: 'Task',
|
|
313
|
+
boardId: 'b1',
|
|
314
|
+
boardName: 'Board',
|
|
315
|
+
listName: 'List',
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
const result = filterTasks(tasksWithoutStatus, null, 'not_started');
|
|
319
|
+
expect(result).toHaveLength(1);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('groupTasksByStatus', () => {
|
|
324
|
+
const tasks = [
|
|
325
|
+
{
|
|
326
|
+
id: '1',
|
|
327
|
+
name: 'Task 1',
|
|
328
|
+
boardId: 'b1',
|
|
329
|
+
boardName: 'Board',
|
|
330
|
+
listName: 'List',
|
|
331
|
+
listStatus: 'not_started',
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: '2',
|
|
335
|
+
name: 'Task 2',
|
|
336
|
+
boardId: 'b1',
|
|
337
|
+
boardName: 'Board',
|
|
338
|
+
listName: 'List',
|
|
339
|
+
listStatus: 'active',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: '3',
|
|
343
|
+
name: 'Task 3',
|
|
344
|
+
boardId: 'b1',
|
|
345
|
+
boardName: 'Board',
|
|
346
|
+
listName: 'List',
|
|
347
|
+
listStatus: 'done',
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: '4',
|
|
351
|
+
name: 'Task 4',
|
|
352
|
+
boardId: 'b1',
|
|
353
|
+
boardName: 'Board',
|
|
354
|
+
listName: 'List',
|
|
355
|
+
listStatus: 'closed',
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: '5',
|
|
359
|
+
name: 'Task 5',
|
|
360
|
+
boardId: 'b1',
|
|
361
|
+
boardName: 'Board',
|
|
362
|
+
listName: 'List',
|
|
363
|
+
archived: true,
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
it('groups tasks by their status', () => {
|
|
368
|
+
const result = groupTasksByStatus(tasks);
|
|
369
|
+
expect(result.not_started).toHaveLength(1);
|
|
370
|
+
expect(result.active).toHaveLength(1);
|
|
371
|
+
expect(result.done).toHaveLength(2); // done + archived
|
|
372
|
+
expect(result.closed).toHaveLength(1);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('puts archived tasks in done group', () => {
|
|
376
|
+
const result = groupTasksByStatus(tasks);
|
|
377
|
+
expect(result.done?.some((t) => t.id === '5')).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('puts tasks without status in not_started group', () => {
|
|
381
|
+
const tasksWithoutStatus = [
|
|
382
|
+
{
|
|
383
|
+
id: '1',
|
|
384
|
+
name: 'Task',
|
|
385
|
+
boardId: 'b1',
|
|
386
|
+
boardName: 'Board',
|
|
387
|
+
listName: 'List',
|
|
388
|
+
},
|
|
389
|
+
];
|
|
390
|
+
const result = groupTasksByStatus(tasksWithoutStatus);
|
|
391
|
+
expect(result.not_started).toHaveLength(1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('returns empty groups for empty input', () => {
|
|
395
|
+
const result = groupTasksByStatus([]);
|
|
396
|
+
expect(result.not_started).toHaveLength(0);
|
|
397
|
+
expect(result.active).toHaveLength(0);
|
|
398
|
+
expect(result.done).toHaveLength(0);
|
|
399
|
+
expect(result.closed).toHaveLength(0);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('getFilteredMetrics', () => {
|
|
404
|
+
const boardMetrics = [
|
|
405
|
+
{
|
|
406
|
+
id: 'board-1',
|
|
407
|
+
totalTasks: 10,
|
|
408
|
+
completedTasks: 5,
|
|
409
|
+
overdueTasks: 2,
|
|
410
|
+
highPriorityTasks: 3,
|
|
411
|
+
progressPercentage: 50,
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: 'board-2',
|
|
415
|
+
totalTasks: 20,
|
|
416
|
+
completedTasks: 15,
|
|
417
|
+
overdueTasks: 1,
|
|
418
|
+
highPriorityTasks: 5,
|
|
419
|
+
progressPercentage: 75,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
id: 'board-3',
|
|
423
|
+
totalTasks: 30,
|
|
424
|
+
completedTasks: 30,
|
|
425
|
+
overdueTasks: 0,
|
|
426
|
+
highPriorityTasks: 10,
|
|
427
|
+
progressPercentage: 100,
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
it('aggregates all boards when no selection', () => {
|
|
432
|
+
const result = getFilteredMetrics(boardMetrics, null);
|
|
433
|
+
expect(result.totalTasks).toBe(60);
|
|
434
|
+
expect(result.totalCompleted).toBe(50);
|
|
435
|
+
expect(result.totalOverdue).toBe(3);
|
|
436
|
+
expect(result.totalHighPriority).toBe(18);
|
|
437
|
+
expect(result.avgProgress).toBe(75); // (50 + 75 + 100) / 3
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('filters by selected board', () => {
|
|
441
|
+
const result = getFilteredMetrics(boardMetrics, 'board-1');
|
|
442
|
+
expect(result.totalTasks).toBe(10);
|
|
443
|
+
expect(result.totalCompleted).toBe(5);
|
|
444
|
+
expect(result.totalOverdue).toBe(2);
|
|
445
|
+
expect(result.totalHighPriority).toBe(3);
|
|
446
|
+
expect(result.avgProgress).toBe(50);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('returns zero values for empty data', () => {
|
|
450
|
+
const result = getFilteredMetrics([], null);
|
|
451
|
+
expect(result.totalTasks).toBe(0);
|
|
452
|
+
expect(result.totalCompleted).toBe(0);
|
|
453
|
+
expect(result.totalOverdue).toBe(0);
|
|
454
|
+
expect(result.totalHighPriority).toBe(0);
|
|
455
|
+
expect(result.avgProgress).toBe(0);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('returns zero values for non-existent board', () => {
|
|
459
|
+
const result = getFilteredMetrics(boardMetrics, 'non-existent');
|
|
460
|
+
expect(result.totalTasks).toBe(0);
|
|
461
|
+
expect(result.avgProgress).toBe(0);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
});
|