@memberjunction/ng-tasks 5.43.0 → 5.45.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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @memberjunction/ng-tasks@5.43.0 build
2
+ > @memberjunction/ng-tasks@5.45.0 build
3
3
  > ngc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # @memberjunction/ng-tasks
2
2
 
3
+ ## 5.45.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [45d121b]
8
+ - Updated dependencies [21e33fe]
9
+ - Updated dependencies [b7cf50f]
10
+ - Updated dependencies [f4f11fa]
11
+ - Updated dependencies [e370816]
12
+ - Updated dependencies [fbee64c]
13
+ - Updated dependencies [b2927f1]
14
+ - Updated dependencies [6125dcd]
15
+ - Updated dependencies [ad9f4a3]
16
+ - Updated dependencies [c1f2d3d]
17
+ - Updated dependencies [0b1e009]
18
+ - @memberjunction/core@5.45.0
19
+ - @memberjunction/core-entities@5.45.0
20
+ - @memberjunction/ai-engine-base@5.45.0
21
+ - @memberjunction/ai-core-plus@5.45.0
22
+ - @memberjunction/global@5.45.0
23
+ - @memberjunction/ng-shared-generic@5.45.0
24
+
25
+ ## 5.44.0
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies [3633fbb]
30
+ - Updated dependencies [1367fbb]
31
+ - Updated dependencies [5396d90]
32
+ - Updated dependencies [7279819]
33
+ - Updated dependencies [d44e430]
34
+ - Updated dependencies [6f74b17]
35
+ - Updated dependencies [be5ab50]
36
+ - Updated dependencies [aa9102d]
37
+ - Updated dependencies [2f926df]
38
+ - Updated dependencies [863a10d]
39
+ - Updated dependencies [2f9b863]
40
+ - @memberjunction/ai-engine-base@5.44.0
41
+ - @memberjunction/ai-core-plus@5.44.0
42
+ - @memberjunction/core-entities@5.44.0
43
+ - @memberjunction/core@5.44.0
44
+ - @memberjunction/global@5.44.0
45
+ - @memberjunction/ng-shared-generic@5.44.0
46
+
3
47
  ## 5.43.0
4
48
 
5
49
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-tasks",
3
- "version": "5.43.0",
3
+ "version": "5.45.0",
4
4
  "description": "MemberJunction: Angular components for task visualization and management with Gantt chart support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,12 +15,12 @@
15
15
  "@angular/common": "21.1.3",
16
16
  "@angular/core": "21.1.3",
17
17
  "@angular/platform-browser": "21.1.3",
18
- "@memberjunction/ai-core-plus": "5.43.0",
19
- "@memberjunction/ai-engine-base": "5.43.0",
20
- "@memberjunction/core": "5.43.0",
21
- "@memberjunction/core-entities": "5.43.0",
22
- "@memberjunction/global": "5.43.0",
23
- "@memberjunction/ng-shared-generic": "5.43.0",
18
+ "@memberjunction/ai-core-plus": "5.45.0",
19
+ "@memberjunction/ai-engine-base": "5.45.0",
20
+ "@memberjunction/core": "5.45.0",
21
+ "@memberjunction/core-entities": "5.45.0",
22
+ "@memberjunction/global": "5.45.0",
23
+ "@memberjunction/ng-shared-generic": "5.45.0",
24
24
  "dhtmlx-gantt": "^9.1.1",
25
25
  "rxjs": "^7.8.2"
26
26
  },
@@ -35,6 +35,7 @@
35
35
  "url": "https://github.com/MemberJunction/MJ"
36
36
  },
37
37
  "devDependencies": {
38
+ "@memberjunction/ng-test-utils": "5.45.0",
38
39
  "vitest": "^4.0.18"
39
40
  }
40
41
  }
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderComponentFixture, query, queryAll, text, hasClass, click, capture } from '@memberjunction/ng-test-utils';
3
+ import { SimpleTaskViewerComponent } from './simple-task-viewer.component';
4
+
5
+ /**
6
+ * DOM-level spec for <mj-simple-task-viewer>. tasks/@Input is MJTaskEntity[]; the template
7
+ * only reads plain fields, so plain objects via `inputs` suffice.
8
+ *
9
+ * Clicking a task sets selectedTask and renders the child <mj-task-detail-panel>. That child's
10
+ * ngOnInit only touches AIEngineBase when task.AgentID is set, so all task fixtures here OMIT
11
+ * AgentID — no engine/backend call. selectedTask is set via a real (click) DOM event, which
12
+ * marks the view dirty the zoneless-correct way (guide §5), so the follow-up render is clean.
13
+ */
14
+ describe('SimpleTaskViewerComponent (DOM)', () => {
15
+ const tasks = [
16
+ { ID: 'a1b2c3d4-0000-0000-0000-000000000001', Name: 'First task', Status: 'In Progress', PercentComplete: 30 },
17
+ { ID: 'a1b2c3d4-0000-0000-0000-000000000002', Name: 'Second task', Status: 'Complete', PercentComplete: 100 },
18
+ ];
19
+
20
+ it('renders one row per task with its title', () => {
21
+ const f = renderComponentFixture(SimpleTaskViewerComponent, { inputs: { tasks } });
22
+ const items = queryAll(f, '.task-item');
23
+ expect(items.length).toBe(2);
24
+ const titles = queryAll(f, '.task-title').map((el) => el.textContent?.trim());
25
+ expect(titles[0]).toContain('First task');
26
+ expect(titles[1]).toContain('Second task');
27
+ });
28
+
29
+ it('marks the completed task with the completed class and check icon', () => {
30
+ const f = renderComponentFixture(SimpleTaskViewerComponent, { inputs: { tasks } });
31
+ const items = queryAll(f, '.task-item');
32
+ expect(items[0].classList.contains('completed')).toBe(false);
33
+ expect(items[1].classList.contains('completed')).toBe(true);
34
+ // completed-check icon only renders for the Complete task
35
+ expect(items[0].querySelector('.completed-check')).toBeNull();
36
+ expect(items[1].querySelector('.completed-check')).not.toBeNull();
37
+ });
38
+
39
+ it('renders the compact progress fill width from PercentComplete', () => {
40
+ const f = renderComponentFixture(SimpleTaskViewerComponent, { inputs: { tasks } });
41
+ const fills = queryAll(f, '.progress-fill-compact') as HTMLElement[];
42
+ expect(fills[0].style.width).toBe('30%');
43
+ expect(fills[1].style.width).toBe('100%');
44
+ // the completed task's fill carries the .complete modifier
45
+ expect(fills[1].classList.contains('complete')).toBe(true);
46
+ });
47
+
48
+ it('shows the empty state when there are no tasks', () => {
49
+ const f = renderComponentFixture(SimpleTaskViewerComponent, { inputs: { tasks: [] } });
50
+ expect(query(f, '.no-tasks')).not.toBeNull();
51
+ expect(text(f, '.no-tasks p')).toBe('No tasks to display');
52
+ expect(queryAll(f, '.task-item').length).toBe(0);
53
+ });
54
+
55
+ it('emits taskClicked and selects the row when a task is clicked', () => {
56
+ const f = renderComponentFixture(SimpleTaskViewerComponent, { inputs: { tasks } });
57
+ const clicked = capture(f.componentInstance.taskClicked);
58
+
59
+ click(f, '.task-item'); // first row
60
+ f.detectChanges(); // re-render after click set selectedTask (view already marked dirty by the event)
61
+
62
+ expect(clicked).toHaveLength(1);
63
+ expect(clicked[0].Name).toBe('First task');
64
+ // selecting a task adds .selected to the first row and reveals the detail panel
65
+ expect(queryAll(f, '.task-item')[0].classList.contains('selected')).toBe(true);
66
+ expect(query(f, 'mj-task-detail-panel')).not.toBeNull();
67
+ });
68
+
69
+ it('does not render the detail panel before any task is selected', () => {
70
+ const f = renderComponentFixture(SimpleTaskViewerComponent, { inputs: { tasks } });
71
+ expect(query(f, 'mj-task-detail-panel')).toBeNull();
72
+ expect(hasClass(f, '.task-list', 'with-detail')).toBe(false);
73
+ });
74
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderComponentFixture, query, queryAll, text, click, capture } from '@memberjunction/ng-test-utils';
3
+ import { TaskDetailPanelComponent } from './task-detail-panel.component';
4
+
5
+ /**
6
+ * DOM-level spec for <mj-task-detail-panel>. The @Input() task is typed as MJTaskEntity,
7
+ * but the template only reads plain fields (Name, Description, Status, PercentComplete,
8
+ * dates, User) — a plain object via `inputs` is enough (see slot specs in conversations).
9
+ *
10
+ * IMPORTANT: ngOnInit/ngOnChanges call loadAgentInfo(), which only touches AIEngineBase
11
+ * when task.AgentID is set. Every task fixture here OMITS AgentID, so loadAgentInfo
12
+ * returns early and no engine/backend call happens — the agent section stays hidden.
13
+ */
14
+ describe('TaskDetailPanelComponent (DOM)', () => {
15
+ const baseTask = { Name: 'Build report', Status: 'In Progress' };
16
+
17
+ it('renders the task name and status', () => {
18
+ const f = renderComponentFixture(TaskDetailPanelComponent, { inputs: { task: baseTask } });
19
+ expect(text(f, '.detail-header h3')).toBe('Build report');
20
+ // Status is always shown (no @if gate); find the field whose label is "Status".
21
+ const fields = queryAll(f, '.detail-field');
22
+ const statusField = fields.find((el) => el.querySelector('label')?.textContent?.trim() === 'Status');
23
+ expect(statusField?.querySelector('p')?.textContent?.trim()).toBe('In Progress');
24
+ });
25
+
26
+ it('hides the Description field when task has no description', () => {
27
+ const f = renderComponentFixture(TaskDetailPanelComponent, { inputs: { task: baseTask } });
28
+ const labels = queryAll(f, '.detail-field label').map((el) => el.textContent?.trim());
29
+ expect(labels).not.toContain('Description');
30
+ });
31
+
32
+ it('shows the Description field when task has a description', () => {
33
+ const f = renderComponentFixture(TaskDetailPanelComponent, {
34
+ inputs: { task: { ...baseTask, Description: 'Quarterly numbers' } },
35
+ });
36
+ const labels = queryAll(f, '.detail-field label').map((el) => el.textContent?.trim());
37
+ expect(labels).toContain('Description');
38
+ const descField = queryAll(f, '.detail-field').find((el) => el.querySelector('label')?.textContent?.trim() === 'Description');
39
+ expect(descField?.querySelector('p')?.textContent?.trim()).toBe('Quarterly numbers');
40
+ });
41
+
42
+ it('renders the progress fill width from PercentComplete', () => {
43
+ const f = renderComponentFixture(TaskDetailPanelComponent, {
44
+ inputs: { task: { ...baseTask, PercentComplete: 42 } },
45
+ });
46
+ const fill = query(f, '.progress-fill-detail') as HTMLElement;
47
+ expect(fill).not.toBeNull();
48
+ expect(fill.style.width).toBe('42%');
49
+ expect(text(f, '.detail-progress span')).toBe('42%');
50
+ });
51
+
52
+ it('hides the progress field when PercentComplete is null', () => {
53
+ const f = renderComponentFixture(TaskDetailPanelComponent, {
54
+ inputs: { task: { ...baseTask, PercentComplete: null } },
55
+ });
56
+ expect(query(f, '.detail-progress')).toBeNull();
57
+ });
58
+
59
+ it('does not render the agent section when there is no agent', () => {
60
+ const f = renderComponentFixture(TaskDetailPanelComponent, { inputs: { task: baseTask } });
61
+ expect(query(f, '.agent-info')).toBeNull();
62
+ });
63
+
64
+ it('shows the agent-run link when agentRunId is set, and emits openEntityRecord on click', () => {
65
+ const f = renderComponentFixture(TaskDetailPanelComponent, {
66
+ inputs: { task: baseTask, agentRunId: 'run-123' },
67
+ });
68
+ const link = query(f, '.agent-run-link');
69
+ expect(link).not.toBeNull();
70
+
71
+ const emitted = capture(f.componentInstance.openEntityRecord);
72
+ click(f, '.agent-run-link');
73
+ expect(emitted).toHaveLength(1);
74
+ expect(emitted[0]).toEqual({ entityName: 'MJ: AI Agent Runs', recordId: 'run-123' });
75
+ });
76
+
77
+ it('hides the agent-run link when agentRunId is null', () => {
78
+ const f = renderComponentFixture(TaskDetailPanelComponent, {
79
+ inputs: { task: baseTask, agentRunId: null },
80
+ });
81
+ expect(query(f, '.agent-run-link')).toBeNull();
82
+ });
83
+
84
+ it('emits closePanel when the close button is clicked', () => {
85
+ const f = renderComponentFixture(TaskDetailPanelComponent, { inputs: { task: baseTask } });
86
+ const closed = capture(f.componentInstance.closePanel);
87
+ click(f, '.close-detail-btn');
88
+ expect(closed).toHaveLength(1);
89
+ });
90
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderComponentFixture, query, queryAll, text, capture } from '@memberjunction/ng-test-utils';
3
+ import { TaskComponent } from './task.component';
4
+
5
+ /**
6
+ * DOM-level spec for <mj-task> — the compound component that hosts the header/view-toggle
7
+ * and switches between the Simple and Gantt viewers.
8
+ *
9
+ * We test ONLY in the default 'simple' viewMode. Switching to 'gantt' renders
10
+ * <mj-gantt-task-viewer>, which boots the dhtmlx-gantt canvas library in ngAfterViewInit
11
+ * (not jsdom-friendly) — that path is deferred. So we assert the toggle's wiring via the
12
+ * "List" button (which keeps viewMode === 'simple') and the @Output, never by rendering gantt.
13
+ */
14
+ describe('TaskComponent (DOM)', () => {
15
+ it('renders the header with title and description when showHeader is true', () => {
16
+ const f = renderComponentFixture(TaskComponent, {
17
+ inputs: { tasks: [], title: 'My Work', description: 'Things to do' },
18
+ });
19
+ expect(query(f, '.task-header')).not.toBeNull();
20
+ expect(text(f, '.task-title')).toBe('My Work');
21
+ expect(text(f, '.task-description')).toBe('Things to do');
22
+ });
23
+
24
+ it('hides the header entirely when showHeader is false', () => {
25
+ const f = renderComponentFixture(TaskComponent, {
26
+ inputs: { tasks: [], showHeader: false, title: 'Hidden' },
27
+ });
28
+ expect(query(f, '.task-header')).toBeNull();
29
+ expect(query(f, '.task-title')).toBeNull();
30
+ });
31
+
32
+ it('omits the title element when no title is provided', () => {
33
+ const f = renderComponentFixture(TaskComponent, { inputs: { tasks: [] } });
34
+ expect(query(f, '.task-header')).not.toBeNull();
35
+ expect(query(f, '.task-title')).toBeNull();
36
+ });
37
+
38
+ it('renders the view toggle by default and marks the List button active in simple mode', () => {
39
+ const f = renderComponentFixture(TaskComponent, { inputs: { tasks: [] } });
40
+ const buttons = queryAll(f, '.toggle-btn');
41
+ expect(buttons.length).toBe(2);
42
+ const [ganttBtn, listBtn] = buttons;
43
+ // default viewMode === 'simple' → List active, Gantt not
44
+ expect(listBtn.classList.contains('active')).toBe(true);
45
+ expect(ganttBtn.classList.contains('active')).toBe(false);
46
+ });
47
+
48
+ it('hides the view toggle when showViewToggle is false', () => {
49
+ const f = renderComponentFixture(TaskComponent, { inputs: { tasks: [], showViewToggle: false } });
50
+ expect(query(f, '.view-toggle')).toBeNull();
51
+ });
52
+
53
+ it('renders the simple viewer in the default simple mode', () => {
54
+ const f = renderComponentFixture(TaskComponent, { inputs: { tasks: [] } });
55
+ expect(query(f, 'mj-simple-task-viewer')).not.toBeNull();
56
+ expect(query(f, 'mj-gantt-task-viewer')).toBeNull();
57
+ });
58
+
59
+ it('emits viewModeChanged when the (already-active) List toggle is clicked, staying in simple mode', () => {
60
+ const f = renderComponentFixture(TaskComponent, { inputs: { tasks: [] } });
61
+ const changed = capture(f.componentInstance.viewModeChanged);
62
+
63
+ // second toggle button is "List" → setViewMode('simple'); keeps us out of the gantt render path
64
+ const listBtn = queryAll(f, '.toggle-btn')[1] as HTMLButtonElement;
65
+ listBtn.click();
66
+ f.detectChanges();
67
+
68
+ expect(changed).toHaveLength(1);
69
+ expect(changed[0]).toBe('simple');
70
+ expect(query(f, 'mj-gantt-task-viewer')).toBeNull();
71
+ });
72
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["vitest/globals", "node"]
5
+ },
6
+ "include": [
7
+ "src/**/*.ts"
8
+ ],
9
+ "exclude": [
10
+ "node_modules",
11
+ "dist"
12
+ ]
13
+ }
package/vitest.config.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { defineProject, mergeConfig } from 'vitest/config';
2
- import sharedConfig from '../../../../vitest.shared';
2
+ import domSharedConfig from '../../../../vitest.dom.shared';
3
3
 
4
+ // Single DOM preset: this package has no class-level spec that vi.mock('@angular/core'),
5
+ // so the existing node/logic specs (index.test.ts, task-models.test.ts) run fine under
6
+ // jsdom alongside the DOM component specs. See guides/ANGULAR_TESTING_GUIDE.md §3b.
4
7
  export default mergeConfig(
5
- sharedConfig,
8
+ domSharedConfig,
6
9
  defineProject({
7
10
  test: {
8
- environment: 'node',
11
+ name: '@memberjunction/ng-tasks',
9
12
  },
10
- })
13
+ }),
11
14
  );
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=index.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
@@ -1,7 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- describe('@memberjunction/ng-tasks', () => {
3
- it('should have a passing test', () => {
4
- expect(true).toBe(true);
5
- });
6
- });
7
- //# sourceMappingURL=index.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it, expect } from 'vitest';\n\ndescribe('@memberjunction/ng-tasks', () => {\n it('should have a passing test', () => {\n expect(true).toBe(true);\n });\n});\n"]}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=task-models.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"task-models.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/task-models.test.ts"],"names":[],"mappings":""}
@@ -1,49 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- describe('TaskViewMode', () => {
3
- it('should accept simple mode', () => {
4
- const mode = 'simple';
5
- expect(mode).toBe('simple');
6
- });
7
- it('should accept gantt mode', () => {
8
- const mode = 'gantt';
9
- expect(mode).toBe('gantt');
10
- });
11
- });
12
- describe('GanttTask', () => {
13
- it('should construct a basic gantt task', () => {
14
- const task = {
15
- id: 'task-1',
16
- name: 'Design Phase',
17
- start: '2026-01-01',
18
- end: '2026-01-15',
19
- progress: 75
20
- };
21
- expect(task.id).toBe('task-1');
22
- expect(task.progress).toBe(75);
23
- expect(task.dependencies).toBeUndefined();
24
- });
25
- it('should support dependencies and custom class', () => {
26
- const task = {
27
- id: 'task-2',
28
- name: 'Implementation',
29
- start: '2026-01-16',
30
- end: '2026-02-15',
31
- progress: 30,
32
- dependencies: 'task-1',
33
- custom_class: 'critical-task'
34
- };
35
- expect(task.dependencies).toBe('task-1');
36
- expect(task.custom_class).toBe('critical-task');
37
- });
38
- it('should handle zero progress', () => {
39
- const task = {
40
- id: 'task-3',
41
- name: 'Future Task',
42
- start: '2026-03-01',
43
- end: '2026-03-30',
44
- progress: 0
45
- };
46
- expect(task.progress).toBe(0);
47
- });
48
- });
49
- //# sourceMappingURL=task-models.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"task-models.test.js","sourceRoot":"","sources":["../../src/__tests__/task-models.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAG9C,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,IAAI,GAAiB,QAAQ,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,IAAI,GAAiB,OAAO,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,IAAI,GAAc;YACtB,EAAE,EAAE,QAAQ;YACZ,IAAI,EAAE,cAAc;YACpB,KAAK,EAAE,YAAY;YACnB,GAAG,EAAE,YAAY;YACjB,QAAQ,EAAE,EAAE;SACb,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/B,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/B,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,aAAa,EAAE,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,IAAI,GAAc;YACtB,EAAE,EAAE,QAAQ;YACZ,IAAI,EAAE,gBAAgB;YACtB,KAAK,EAAE,YAAY;YACnB,GAAG,EAAE,YAAY;YACjB,QAAQ,EAAE,EAAE;YACZ,YAAY,EAAE,QAAQ;YACtB,YAAY,EAAE,eAAe;SAC9B,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,IAAI,GAAc;YACtB,EAAE,EAAE,QAAQ;YACZ,IAAI,EAAE,aAAa;YACnB,KAAK,EAAE,YAAY;YACnB,GAAG,EAAE,YAAY;YACjB,QAAQ,EAAE,CAAC;SACZ,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport type { TaskViewMode, GanttTask } from '../lib/models/task-view.models';\n\ndescribe('TaskViewMode', () => {\n it('should accept simple mode', () => {\n const mode: TaskViewMode = 'simple';\n expect(mode).toBe('simple');\n });\n\n it('should accept gantt mode', () => {\n const mode: TaskViewMode = 'gantt';\n expect(mode).toBe('gantt');\n });\n});\n\ndescribe('GanttTask', () => {\n it('should construct a basic gantt task', () => {\n const task: GanttTask = {\n id: 'task-1',\n name: 'Design Phase',\n start: '2026-01-01',\n end: '2026-01-15',\n progress: 75\n };\n expect(task.id).toBe('task-1');\n expect(task.progress).toBe(75);\n expect(task.dependencies).toBeUndefined();\n });\n\n it('should support dependencies and custom class', () => {\n const task: GanttTask = {\n id: 'task-2',\n name: 'Implementation',\n start: '2026-01-16',\n end: '2026-02-15',\n progress: 30,\n dependencies: 'task-1',\n custom_class: 'critical-task'\n };\n expect(task.dependencies).toBe('task-1');\n expect(task.custom_class).toBe('critical-task');\n });\n\n it('should handle zero progress', () => {\n const task: GanttTask = {\n id: 'task-3',\n name: 'Future Task',\n start: '2026-03-01',\n end: '2026-03-30',\n progress: 0\n };\n expect(task.progress).toBe(0);\n });\n});\n"]}