@farm-investimentos/front-mfe-components 15.12.1 → 15.13.1
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/dist/front-mfe-components.common.js +4483 -90
- package/dist/front-mfe-components.common.js.map +1 -1
- package/dist/front-mfe-components.css +1 -1
- package/dist/front-mfe-components.umd.js +4483 -90
- package/dist/front-mfe-components.umd.js.map +1 -1
- package/dist/front-mfe-components.umd.min.js +1 -1
- package/dist/front-mfe-components.umd.min.js.map +1 -1
- package/package.json +2 -1
- package/src/components/GanttChart/GanttChart.scss +223 -0
- package/src/components/GanttChart/GanttChart.stories.js +480 -0
- package/src/components/GanttChart/GanttChart.vue +308 -0
- package/src/components/GanttChart/__tests__/GanttChart.spec.js +561 -0
- package/src/components/GanttChart/composition/buildBarPositioning.ts +145 -0
- package/src/components/GanttChart/composition/buildGanttData.ts +61 -0
- package/src/components/GanttChart/composition/index.ts +2 -0
- package/src/components/GanttChart/index.ts +4 -0
- package/src/components/GanttChart/types/index.ts +60 -0
- package/src/components/GanttChart/utils/dateUtils.ts +98 -0
- package/src/main.ts +2 -1
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import GanttChart from '../GanttChart.vue';
|
|
3
|
+
|
|
4
|
+
describe('GanttChart component', () => {
|
|
5
|
+
let wrapper;
|
|
6
|
+
let component;
|
|
7
|
+
|
|
8
|
+
const defaultProps = {
|
|
9
|
+
data: {
|
|
10
|
+
groups: [
|
|
11
|
+
{
|
|
12
|
+
title: 'Test Group',
|
|
13
|
+
bars: [
|
|
14
|
+
{
|
|
15
|
+
id: 1,
|
|
16
|
+
label: 'Test Bar',
|
|
17
|
+
start: new Date(2025, 0, 1),
|
|
18
|
+
end: new Date(2025, 1, 1),
|
|
19
|
+
color: '#7BC4F7',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
wrapper = shallowMount(GanttChart, {
|
|
29
|
+
propsData: defaultProps,
|
|
30
|
+
});
|
|
31
|
+
component = wrapper.vm;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('Created hook', () => {
|
|
35
|
+
expect(wrapper).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('mount component', () => {
|
|
39
|
+
it('renders correctly', () => {
|
|
40
|
+
expect(wrapper.element).toMatchSnapshot();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders groups correctly', () => {
|
|
44
|
+
expect(wrapper.findAll('.farm-gantt-chart__group')).toHaveLength(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders month headers', () => {
|
|
48
|
+
const monthHeaders = wrapper.findAll('.farm-gantt-chart__month-header');
|
|
49
|
+
expect(monthHeaders.length).toBeGreaterThan(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('displays group titles correctly', () => {
|
|
53
|
+
const groupLabel = wrapper.find('.farm-gantt-chart__group-label');
|
|
54
|
+
expect(groupLabel.text()).toContain('Test Group');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Props', () => {
|
|
59
|
+
it('accepts data prop with new structure', () => {
|
|
60
|
+
expect(component.data).toEqual(defaultProps.data);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('validates required data prop structure', () => {
|
|
64
|
+
expect(component.data.groups).toBeDefined();
|
|
65
|
+
expect(Array.isArray(component.data.groups)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('validates group structure', () => {
|
|
69
|
+
const group = component.data.groups[0];
|
|
70
|
+
expect(group.title).toBeDefined();
|
|
71
|
+
expect(group.bars).toBeDefined();
|
|
72
|
+
expect(Array.isArray(group.bars)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('validates bar structure', () => {
|
|
76
|
+
const bar = component.data.groups[0].bars[0];
|
|
77
|
+
expect(bar.id).toBeDefined();
|
|
78
|
+
expect(bar.label).toBeDefined();
|
|
79
|
+
expect(bar.start).toBeDefined();
|
|
80
|
+
expect(bar.end).toBeDefined();
|
|
81
|
+
expect(bar.color).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Automatic Calculations', () => {
|
|
86
|
+
describe('Date Range Calculation', () => {
|
|
87
|
+
it('should calculate date range automatically from bars', () => {
|
|
88
|
+
const testData = {
|
|
89
|
+
groups: [
|
|
90
|
+
{
|
|
91
|
+
title: 'Group 1',
|
|
92
|
+
bars: [
|
|
93
|
+
{
|
|
94
|
+
id: 1,
|
|
95
|
+
label: 'Early Bar',
|
|
96
|
+
start: new Date(2025, 0, 15), // Jan 15, 2025
|
|
97
|
+
end: new Date(2025, 2, 10), // Mar 10, 2025
|
|
98
|
+
color: '#7BC4F7',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 2,
|
|
102
|
+
label: 'Late Bar',
|
|
103
|
+
start: new Date(2025, 4, 5), // May 5, 2025
|
|
104
|
+
end: new Date(2025, 6, 20), // Jul 20, 2025
|
|
105
|
+
color: '#8BB455',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const testWrapper = shallowMount(GanttChart, {
|
|
113
|
+
propsData: { data: testData },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Should calculate from January 1st to July 31st (full months)
|
|
117
|
+
const monthColumns = testWrapper.vm.monthColumns;
|
|
118
|
+
expect(monthColumns.length).toBe(7); // Jan to Jul = 7 months
|
|
119
|
+
expect(monthColumns[0].label).toContain('Jan');
|
|
120
|
+
expect(monthColumns[monthColumns.length - 1].label).toContain('Jul');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should handle single month range', () => {
|
|
124
|
+
const testData = {
|
|
125
|
+
groups: [
|
|
126
|
+
{
|
|
127
|
+
title: 'Single Month Group',
|
|
128
|
+
bars: [
|
|
129
|
+
{
|
|
130
|
+
id: 1,
|
|
131
|
+
label: 'Single Month Bar',
|
|
132
|
+
start: new Date(2025, 3, 5), // Apr 5, 2025
|
|
133
|
+
end: new Date(2025, 3, 25), // Apr 25, 2025
|
|
134
|
+
color: '#7BC4F7',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const testWrapper = shallowMount(GanttChart, {
|
|
142
|
+
propsData: { data: testData },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const monthColumns = testWrapper.vm.monthColumns;
|
|
146
|
+
expect(monthColumns.length).toBe(1);
|
|
147
|
+
expect(monthColumns[0].label).toContain('Abr');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Legend Generation', () => {
|
|
152
|
+
it('should generate legend automatically from unique colors and labels', () => {
|
|
153
|
+
const testData = {
|
|
154
|
+
groups: [
|
|
155
|
+
{
|
|
156
|
+
title: 'Group 1',
|
|
157
|
+
bars: [
|
|
158
|
+
{
|
|
159
|
+
id: 1,
|
|
160
|
+
label: 'Design',
|
|
161
|
+
start: new Date(2025, 0, 1),
|
|
162
|
+
end: new Date(2025, 1, 1),
|
|
163
|
+
color: '#8E44AD',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 2,
|
|
167
|
+
label: 'Development',
|
|
168
|
+
start: new Date(2025, 1, 1),
|
|
169
|
+
end: new Date(2025, 2, 1),
|
|
170
|
+
color: '#16A085',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 3,
|
|
174
|
+
label: 'Design', // Same label, same color - should not duplicate
|
|
175
|
+
start: new Date(2025, 2, 1),
|
|
176
|
+
end: new Date(2025, 3, 1),
|
|
177
|
+
color: '#8E44AD',
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const testWrapper = shallowMount(GanttChart, {
|
|
185
|
+
propsData: { data: testData },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const legend = testWrapper.vm.autoGeneratedLegend;
|
|
189
|
+
expect(legend).toHaveLength(2); // Only unique combinations
|
|
190
|
+
expect(legend.some(item => item.label === 'Design' && item.color === '#8E44AD')).toBe(true);
|
|
191
|
+
expect(legend.some(item => item.label === 'Development' && item.color === '#16A085')).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should display generated legend in template', () => {
|
|
195
|
+
const legendItems = wrapper.findAll('.farm-gantt-chart__legend-item');
|
|
196
|
+
expect(legendItems.length).toBeGreaterThan(0);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('Methods', () => {
|
|
202
|
+
describe('getPositionedBars', () => {
|
|
203
|
+
it('should position bars correctly with each bar in its own row', () => {
|
|
204
|
+
const bars = [
|
|
205
|
+
{
|
|
206
|
+
id: 1,
|
|
207
|
+
start: new Date(2025, 0, 1),
|
|
208
|
+
end: new Date(2025, 1, 1),
|
|
209
|
+
label: 'Bar 1',
|
|
210
|
+
color: '#7BC4F7',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 2,
|
|
214
|
+
start: new Date(2025, 0, 15),
|
|
215
|
+
end: new Date(2025, 1, 15),
|
|
216
|
+
label: 'Bar 2',
|
|
217
|
+
color: '#8BB455',
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
const positionedBars = component.getPositionedBars(bars);
|
|
221
|
+
expect(positionedBars).toHaveLength(2);
|
|
222
|
+
// Each bar should have its own unique row position
|
|
223
|
+
expect(positionedBars[0].rowPosition).toBe(0);
|
|
224
|
+
expect(positionedBars[1].rowPosition).toBe(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should maintain original order and position bars in separate rows', () => {
|
|
228
|
+
const bars = [
|
|
229
|
+
{
|
|
230
|
+
id: 3,
|
|
231
|
+
start: new Date(2025, 2, 1), // Latest date but first in array
|
|
232
|
+
end: new Date(2025, 2, 15),
|
|
233
|
+
label: 'Latest Bar',
|
|
234
|
+
color: '#FFB84D',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 1,
|
|
238
|
+
start: new Date(2025, 0, 1), // Earliest date but second in array
|
|
239
|
+
end: new Date(2025, 0, 15),
|
|
240
|
+
label: 'Early Bar',
|
|
241
|
+
color: '#7BC4F7',
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 2,
|
|
245
|
+
start: new Date(2025, 1, 1), // Middle date but third in array
|
|
246
|
+
end: new Date(2025, 1, 15),
|
|
247
|
+
label: 'Middle Bar',
|
|
248
|
+
color: '#8BB455',
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
const positionedBars = component.getPositionedBars(bars);
|
|
252
|
+
expect(positionedBars).toHaveLength(3);
|
|
253
|
+
// Bars should maintain original order, not be sorted by date
|
|
254
|
+
expect(positionedBars[0].id).toBe(3); // Latest date stays first
|
|
255
|
+
expect(positionedBars[1].id).toBe(1); // Earliest date stays second
|
|
256
|
+
expect(positionedBars[2].id).toBe(2); // Middle date stays third
|
|
257
|
+
expect(positionedBars[0].rowPosition).toBe(0);
|
|
258
|
+
expect(positionedBars[1].rowPosition).toBe(1);
|
|
259
|
+
expect(positionedBars[2].rowPosition).toBe(2);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should handle empty bars array', () => {
|
|
263
|
+
const positionedBars = component.getPositionedBars([]);
|
|
264
|
+
expect(positionedBars).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should handle invalid dates gracefully', () => {
|
|
268
|
+
const bars = [
|
|
269
|
+
{
|
|
270
|
+
id: 1,
|
|
271
|
+
start: 'invalid-date',
|
|
272
|
+
end: 'invalid-date',
|
|
273
|
+
label: 'Invalid Bar',
|
|
274
|
+
color: '#7BC4F7',
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
const positionedBars = component.getPositionedBars(bars);
|
|
278
|
+
expect(positionedBars).toHaveLength(1);
|
|
279
|
+
expect(positionedBars[0].rowPosition).toBeDefined();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('getBarGridStyle', () => {
|
|
284
|
+
it('should return correct grid style for bars', () => {
|
|
285
|
+
const bar = {
|
|
286
|
+
id: 1,
|
|
287
|
+
start: new Date(2025, 0, 1),
|
|
288
|
+
end: new Date(2025, 1, 1),
|
|
289
|
+
label: 'Test Bar',
|
|
290
|
+
color: '#7BC4F7',
|
|
291
|
+
rowPosition: 0,
|
|
292
|
+
};
|
|
293
|
+
const style = component.getBarGridStyle(bar);
|
|
294
|
+
expect(style['background-color']).toBe('#7BC4F7');
|
|
295
|
+
expect(style['grid-row']).toBe('1');
|
|
296
|
+
expect(style['grid-column-start']).toBeDefined();
|
|
297
|
+
expect(style['grid-column-end']).toBeDefined();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('Color Fallback', () => {
|
|
304
|
+
it('should use fallback color when bar.color is not provided', () => {
|
|
305
|
+
const testData = {
|
|
306
|
+
groups: [{
|
|
307
|
+
title: 'Test Group',
|
|
308
|
+
bars: [{
|
|
309
|
+
id: 1,
|
|
310
|
+
label: 'Bar Without Color',
|
|
311
|
+
start: new Date(2025, 0, 1),
|
|
312
|
+
end: new Date(2025, 1, 1),
|
|
313
|
+
// color não definido - deve usar fallback
|
|
314
|
+
}]
|
|
315
|
+
}]
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const testWrapper = shallowMount(GanttChart, {
|
|
319
|
+
propsData: { data: testData },
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const bar = {
|
|
323
|
+
id: 1,
|
|
324
|
+
label: 'Bar Without Color',
|
|
325
|
+
start: new Date(2025, 0, 1),
|
|
326
|
+
end: new Date(2025, 1, 1),
|
|
327
|
+
rowPosition: 0
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const style = testWrapper.vm.getBarGridStyle(bar);
|
|
331
|
+
expect(style['background-color']).toBe('var(--farm-primary-base)');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should use provided color when bar.color is defined', () => {
|
|
335
|
+
const bar = {
|
|
336
|
+
id: 1,
|
|
337
|
+
label: 'Bar With Color',
|
|
338
|
+
start: new Date(2025, 0, 1),
|
|
339
|
+
end: new Date(2025, 1, 1),
|
|
340
|
+
color: '#FF5733',
|
|
341
|
+
rowPosition: 0
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const style = component.getBarGridStyle(bar);
|
|
345
|
+
expect(style['background-color']).toBe('#FF5733');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('Data Validation', () => {
|
|
350
|
+
it('should validate invalid data prop structure', () => {
|
|
351
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
352
|
+
|
|
353
|
+
// Teste com data null
|
|
354
|
+
const validator = wrapper.vm.$options.props.data.validator;
|
|
355
|
+
expect(validator(null)).toBe(false);
|
|
356
|
+
expect(consoleSpy).toHaveBeenCalledWith('GanttChart: prop "data" deve ser um objeto.');
|
|
357
|
+
|
|
358
|
+
// Teste com data sem groups
|
|
359
|
+
expect(validator({})).toBe(false);
|
|
360
|
+
expect(consoleSpy).toHaveBeenCalledWith('GanttChart: prop "data.groups" deve ser um array.');
|
|
361
|
+
|
|
362
|
+
// Teste com groups inválido
|
|
363
|
+
expect(validator({ groups: 'invalid' })).toBe(false);
|
|
364
|
+
expect(consoleSpy).toHaveBeenCalledWith('GanttChart: prop "data.groups" deve ser um array.');
|
|
365
|
+
|
|
366
|
+
consoleSpy.mockRestore();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should validate group structure', () => {
|
|
370
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
371
|
+
const validator = wrapper.vm.$options.props.data.validator;
|
|
372
|
+
|
|
373
|
+
// Teste com grupo sem title
|
|
374
|
+
const invalidData = {
|
|
375
|
+
groups: [{
|
|
376
|
+
bars: []
|
|
377
|
+
}]
|
|
378
|
+
};
|
|
379
|
+
expect(validator(invalidData)).toBe(false);
|
|
380
|
+
expect(consoleSpy).toHaveBeenCalledWith('GanttChart: cada grupo deve ter título (string) e barras (array).');
|
|
381
|
+
|
|
382
|
+
// Teste com grupo sem bars
|
|
383
|
+
const invalidData2 = {
|
|
384
|
+
groups: [{
|
|
385
|
+
title: 'Valid Title'
|
|
386
|
+
}]
|
|
387
|
+
};
|
|
388
|
+
expect(validator(invalidData2)).toBe(false);
|
|
389
|
+
expect(consoleSpy).toHaveBeenCalledWith('GanttChart: cada grupo deve ter título (string) e barras (array).');
|
|
390
|
+
|
|
391
|
+
consoleSpy.mockRestore();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should validate correct data structure', () => {
|
|
395
|
+
const validator = wrapper.vm.$options.props.data.validator;
|
|
396
|
+
const validData = {
|
|
397
|
+
groups: [{
|
|
398
|
+
title: 'Valid Group',
|
|
399
|
+
bars: []
|
|
400
|
+
}]
|
|
401
|
+
};
|
|
402
|
+
expect(validator(validData)).toBe(true);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('Date Handling', () => {
|
|
407
|
+
it('should correct dates when end is before start', () => {
|
|
408
|
+
const bar = {
|
|
409
|
+
id: 1,
|
|
410
|
+
label: 'Inverted Date Bar',
|
|
411
|
+
// Datas invertidas (end antes de start)
|
|
412
|
+
start: new Date(2025, 3, 15), // 15 de abril
|
|
413
|
+
end: new Date(2025, 2, 15), // 15 de março (antes do início)
|
|
414
|
+
color: '#7BC4F7',
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Normalizar barra via função de posicionamento
|
|
418
|
+
const dates = component.normalizeBarDates(bar);
|
|
419
|
+
|
|
420
|
+
// Verificar se as datas foram invertidas corretamente
|
|
421
|
+
expect(dates.startDate.getTime()).toBe(new Date(2025, 2, 15).getTime()); // Agora é 15 de março
|
|
422
|
+
expect(dates.endDate.getTime()).toBe(new Date(2025, 3, 15).getTime()); // Agora é 15 de abril
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should handle invalid dates', () => {
|
|
426
|
+
const bar = {
|
|
427
|
+
id: 1,
|
|
428
|
+
label: 'Invalid Date Bar',
|
|
429
|
+
start: 'invalid-date',
|
|
430
|
+
end: 'invalid-date',
|
|
431
|
+
color: '#7BC4F7',
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const dates = component.normalizeBarDates(bar);
|
|
435
|
+
expect(dates).toBeNull();
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe('Tooltip System', () => {
|
|
440
|
+
it('should show tooltip on bar mouseenter', async () => {
|
|
441
|
+
// Encontrar uma barra
|
|
442
|
+
const bar = wrapper.find('.farm-gantt-chart__bar');
|
|
443
|
+
|
|
444
|
+
if (bar.exists()) {
|
|
445
|
+
// Disparar evento mouseenter
|
|
446
|
+
await bar.trigger('mouseenter', {
|
|
447
|
+
clientX: 100,
|
|
448
|
+
clientY: 100,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Verificar se tooltipState foi atualizado
|
|
452
|
+
expect(component.tooltipState.visible).toBe(true);
|
|
453
|
+
expect(component.tooltipState.title).toBe('Test Bar');
|
|
454
|
+
expect(component.tooltipState.barData).not.toBeNull();
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should hide tooltip on bar mouseleave', async () => {
|
|
459
|
+
// Preparar estado (mostrar tooltip)
|
|
460
|
+
component.tooltipState.visible = true;
|
|
461
|
+
component.tooltipState.title = 'Teste';
|
|
462
|
+
component.tooltipState.barData = { label: 'Teste' };
|
|
463
|
+
|
|
464
|
+
// Encontrar e disparar mouseleave
|
|
465
|
+
const bar = wrapper.find('.farm-gantt-chart__bar');
|
|
466
|
+
if (bar.exists()) {
|
|
467
|
+
await bar.trigger('mouseleave');
|
|
468
|
+
|
|
469
|
+
// Verificar se tooltipState foi atualizado
|
|
470
|
+
expect(component.tooltipState.visible).toBe(false);
|
|
471
|
+
expect(component.tooltipState.barData).toBeNull();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should show structured tooltip when tooltipData is available', () => {
|
|
476
|
+
// Criar wrapper com dados para tooltip
|
|
477
|
+
const testData = {
|
|
478
|
+
groups: [{
|
|
479
|
+
title: 'Group with Tooltips',
|
|
480
|
+
bars: [{
|
|
481
|
+
id: 1,
|
|
482
|
+
label: 'Bar with Tooltip',
|
|
483
|
+
start: new Date(2025, 0, 1),
|
|
484
|
+
end: new Date(2025, 1, 1),
|
|
485
|
+
color: '#7BC4F7',
|
|
486
|
+
tooltipData: {
|
|
487
|
+
'Taxa': '1,75%',
|
|
488
|
+
'Vigência': '01/01/2025 a 31/01/2025'
|
|
489
|
+
}
|
|
490
|
+
}]
|
|
491
|
+
}]
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const tooltipWrapper = shallowMount(GanttChart, {
|
|
495
|
+
propsData: { data: testData },
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Simular mouseenter
|
|
499
|
+
const barWithTooltip = tooltipWrapper.find('.farm-gantt-chart__bar');
|
|
500
|
+
if (barWithTooltip.exists()) {
|
|
501
|
+
barWithTooltip.trigger('mouseenter', {
|
|
502
|
+
clientX: 100,
|
|
503
|
+
clientY: 100
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Verificar que tooltip com dados estruturados será exibido
|
|
507
|
+
// quando tooltipState.barData.tooltipData existir
|
|
508
|
+
expect(tooltipWrapper.vm.tooltipState.barData.tooltipData).toBeDefined();
|
|
509
|
+
expect(tooltipWrapper.vm.tooltipState.barData.tooltipData.Taxa).toBe('1,75%');
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe('Empty Data Handling', () => {
|
|
515
|
+
it('should handle empty groups array gracefully', () => {
|
|
516
|
+
const emptyData = { groups: [] };
|
|
517
|
+
const emptyWrapper = shallowMount(GanttChart, {
|
|
518
|
+
propsData: { data: emptyData },
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Componente deve renderizar sem erros mesmo com dados vazios
|
|
522
|
+
expect(emptyWrapper.exists()).toBe(true);
|
|
523
|
+
expect(emptyWrapper.find('.farm-gantt-chart').exists()).toBe(true);
|
|
524
|
+
|
|
525
|
+
// Não deve haver grupos renderizados
|
|
526
|
+
const groups = emptyWrapper.findAll('.farm-gantt-chart__group');
|
|
527
|
+
expect(groups.length).toBe(0);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('should render timeline headers even with empty data', () => {
|
|
531
|
+
const emptyData = { groups: [] };
|
|
532
|
+
const emptyWrapper = shallowMount(GanttChart, {
|
|
533
|
+
propsData: { data: emptyData },
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Timeline headers devem estar presentes (baseado em data range padrão ou calculado)
|
|
537
|
+
const header = emptyWrapper.find('.farm-gantt-chart__header');
|
|
538
|
+
expect(header.exists()).toBe(true);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe('Computed Properties', () => {
|
|
543
|
+
it('should generate month columns correctly', () => {
|
|
544
|
+
expect(component.monthColumns).toBeDefined();
|
|
545
|
+
expect(Array.isArray(component.monthColumns)).toBe(true);
|
|
546
|
+
expect(component.monthColumns.length).toBeGreaterThan(0);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('should generate timeline grid style', () => {
|
|
550
|
+
expect(component.timelineGridStyle).toBeDefined();
|
|
551
|
+
expect(component.timelineGridStyle.gridTemplateColumns).toBeDefined();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('should calculate component style with height', () => {
|
|
555
|
+
expect(component.componentStyle).toBeDefined();
|
|
556
|
+
expect(component.componentStyle['--gantt-content-height']).toBeDefined();
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { isValid } from 'date-fns';
|
|
2
|
+
import type { GanttBar } from '../types';
|
|
3
|
+
import { getColumnForDate, getDaysInMonth } from '../utils/dateUtils';
|
|
4
|
+
|
|
5
|
+
export default function buildBarPositioning(dateRange, monthColumns) {
|
|
6
|
+
const normalizeBarDates = (bar: GanttBar) => {
|
|
7
|
+
let startDate = bar.start instanceof Date ? bar.start : new Date(bar.start);
|
|
8
|
+
let endDate = bar.end instanceof Date ? bar.end : new Date(bar.end);
|
|
9
|
+
|
|
10
|
+
if (!isValid(startDate) || !isValid(endDate)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (endDate < startDate) {
|
|
15
|
+
const temp = startDate;
|
|
16
|
+
startDate = endDate;
|
|
17
|
+
endDate = temp;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { startDate, endDate };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const calculateSingleMonthPositioning = (startDate: Date, endDate: Date) => {
|
|
24
|
+
const startDay = startDate.getDate();
|
|
25
|
+
const startMonth = startDate.getMonth();
|
|
26
|
+
const startYear = startDate.getFullYear();
|
|
27
|
+
const endDay = endDate.getDate();
|
|
28
|
+
const endMonth = endDate.getMonth();
|
|
29
|
+
const endYear = endDate.getFullYear();
|
|
30
|
+
|
|
31
|
+
const daysInMonth = getDaysInMonth(startYear, startMonth);
|
|
32
|
+
const startFraction = (startDay - 1) / daysInMonth;
|
|
33
|
+
const effectiveEndDay =
|
|
34
|
+
startYear === endYear && startMonth === endMonth ? endDay : daysInMonth;
|
|
35
|
+
const endFraction = effectiveEndDay / daysInMonth;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
marginLeft: `calc(${startFraction * 100}%)`,
|
|
39
|
+
width: `calc(${(endFraction - startFraction) * 100}%)`,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const calculateMultiMonthPositioning = (
|
|
44
|
+
startDate: Date,
|
|
45
|
+
endDate: Date,
|
|
46
|
+
visualMonthsSpanned: number,
|
|
47
|
+
gridColumnsSpanned: number
|
|
48
|
+
) => {
|
|
49
|
+
const startDay = startDate.getDate();
|
|
50
|
+
const endDay = endDate.getDate();
|
|
51
|
+
const startMonth = startDate.getMonth();
|
|
52
|
+
const startYear = startDate.getFullYear();
|
|
53
|
+
const endMonth = endDate.getMonth();
|
|
54
|
+
const endYear = endDate.getFullYear();
|
|
55
|
+
|
|
56
|
+
const daysInStartMonth = getDaysInMonth(startYear, startMonth);
|
|
57
|
+
const daysInEndMonth = getDaysInMonth(endYear, endMonth);
|
|
58
|
+
|
|
59
|
+
const fractionBeforeBar = (startDay - 1) / daysInStartMonth;
|
|
60
|
+
const fractionInStartMonth = (daysInStartMonth - (startDay - 1)) / daysInStartMonth;
|
|
61
|
+
const fractionInEndMonth = endDay / daysInEndMonth;
|
|
62
|
+
const fullIntermediateMonths = Math.max(0, visualMonthsSpanned - 2);
|
|
63
|
+
|
|
64
|
+
if (gridColumnsSpanned <= 0) {
|
|
65
|
+
return { marginLeft: '0%', width: '100%' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const totalBarCoverage = fractionInStartMonth + fullIntermediateMonths + fractionInEndMonth;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
marginLeft: `calc((${fractionBeforeBar} / ${gridColumnsSpanned}) * 100%)`,
|
|
72
|
+
width: `calc((${totalBarCoverage} / ${gridColumnsSpanned}) * 100%)`,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getBarGridStyle = (bar: GanttBar) => {
|
|
77
|
+
const { start: chartStartDate } = dateRange.value;
|
|
78
|
+
|
|
79
|
+
const dates = normalizeBarDates(bar);
|
|
80
|
+
if (!dates) {
|
|
81
|
+
const backgroundColor = bar.color || 'var(--farm-primary-base)';
|
|
82
|
+
return {
|
|
83
|
+
gridColumn: '1 / 2',
|
|
84
|
+
backgroundColor: backgroundColor,
|
|
85
|
+
gridRow: `${(bar.rowPosition || 0) + 1}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { startDate, endDate } = dates;
|
|
90
|
+
|
|
91
|
+
const startColumnIndex = getColumnForDate(startDate, chartStartDate);
|
|
92
|
+
const endColumnIndex = getColumnForDate(endDate, chartStartDate);
|
|
93
|
+
|
|
94
|
+
const gridColumnStart = Math.max(1, startColumnIndex + 1);
|
|
95
|
+
const gridColumnEnd = Math.min(monthColumns.value.length + 1, endColumnIndex + 2);
|
|
96
|
+
const gridColumnsSpanned = gridColumnEnd - gridColumnStart;
|
|
97
|
+
|
|
98
|
+
const visualStartCol = getColumnForDate(startDate, chartStartDate);
|
|
99
|
+
const visualEndCol = getColumnForDate(endDate, chartStartDate);
|
|
100
|
+
const visualMonthsSpanned = visualEndCol - visualStartCol + 1;
|
|
101
|
+
|
|
102
|
+
let positioning;
|
|
103
|
+
if (visualMonthsSpanned === 1) {
|
|
104
|
+
positioning = calculateSingleMonthPositioning(startDate, endDate);
|
|
105
|
+
} else {
|
|
106
|
+
positioning = calculateMultiMonthPositioning(
|
|
107
|
+
startDate,
|
|
108
|
+
endDate,
|
|
109
|
+
visualMonthsSpanned,
|
|
110
|
+
gridColumnsSpanned
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const backgroundColor = bar.color || 'var(--farm-primary-base)';
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
'grid-column-start': gridColumnStart,
|
|
118
|
+
'grid-column-end': gridColumnEnd,
|
|
119
|
+
'background-color': backgroundColor,
|
|
120
|
+
'grid-row': `${(bar.rowPosition || 0) + 1}`,
|
|
121
|
+
'margin-left': positioning.marginLeft,
|
|
122
|
+
width: positioning.width,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const getPositionedBars = (bars: GanttBar[]) => {
|
|
127
|
+
if (!bars || bars.length === 0) return [];
|
|
128
|
+
|
|
129
|
+
const positionedBars = JSON.parse(JSON.stringify(bars));
|
|
130
|
+
|
|
131
|
+
positionedBars.forEach((bar: GanttBar, index: number) => {
|
|
132
|
+
bar.rowPosition = index;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return positionedBars;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
normalizeBarDates,
|
|
140
|
+
calculateSingleMonthPositioning,
|
|
141
|
+
calculateMultiMonthPositioning,
|
|
142
|
+
getBarGridStyle,
|
|
143
|
+
getPositionedBars,
|
|
144
|
+
};
|
|
145
|
+
}
|