@n8n/frontend-module-insights 0.1.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/LICENSE.md +88 -0
- package/README.md +64 -0
- package/biome.jsonc +4 -0
- package/eslint.config.mjs +33 -0
- package/package.json +72 -0
- package/src/__tests__/setup.ts +4 -0
- package/src/chartjs.utils.ts +208 -0
- package/src/components/InsightsDashboard.test.ts +891 -0
- package/src/components/InsightsDashboard.vue +426 -0
- package/src/components/InsightsDataRangePicker.vue +195 -0
- package/src/components/InsightsDateRangeAlert.test.ts +134 -0
- package/src/components/InsightsDateRangeAlert.vue +89 -0
- package/src/components/InsightsPaywall.vue +42 -0
- package/src/components/InsightsSummary.test.ts +206 -0
- package/src/components/InsightsSummary.vue +357 -0
- package/src/components/InsightsUpgradeModal.vue +74 -0
- package/src/components/__snapshots__/InsightsSummary.test.ts.snap +446 -0
- package/src/components/charts/InsightsChartAverageRuntime.vue +67 -0
- package/src/components/charts/InsightsChartFailed.vue +57 -0
- package/src/components/charts/InsightsChartFailureRate.vue +58 -0
- package/src/components/charts/InsightsChartTimeSaved.vue +74 -0
- package/src/components/charts/InsightsChartTotal.vue +58 -0
- package/src/components/charts/insightChartProps.ts +9 -0
- package/src/components/tables/InsightsTableWorkflows.test.ts +509 -0
- package/src/components/tables/InsightsTableWorkflows.vue +279 -0
- package/src/index.ts +4 -0
- package/src/insights.api.test.ts +435 -0
- package/src/insights.api.ts +71 -0
- package/src/insights.constants.ts +69 -0
- package/src/insights.module.test.ts +59 -0
- package/src/insights.module.ts +45 -0
- package/src/insights.store.test.ts +118 -0
- package/src/insights.store.ts +88 -0
- package/src/insights.types.ts +16 -0
- package/src/insights.utils.test.ts +339 -0
- package/src/insights.utils.ts +172 -0
- package/stylelint.config.mjs +9 -0
- package/vite.config.ts +29 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import type { InsightsByWorkflow } from '@n8n/api-types';
|
|
2
|
+
import { createComponentRenderer, useEmitters } from '@n8n/frontend-test-utils';
|
|
3
|
+
import { createTestingPinia } from '@pinia/testing';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { screen, within } from '@testing-library/vue';
|
|
6
|
+
import { vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import InsightsTableWorkflows from './InsightsTableWorkflows.vue';
|
|
9
|
+
|
|
10
|
+
const { emitters, addEmitter } = useEmitters<'n8nDataTableServer'>();
|
|
11
|
+
|
|
12
|
+
const mockTelemetry = {
|
|
13
|
+
track: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
vi.mock('@n8n/composables/useTelemetry', () => ({
|
|
16
|
+
useTelemetry: () => mockTelemetry,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('@n8n/design-system', async (importOriginal) => {
|
|
20
|
+
const original = await importOriginal<object>();
|
|
21
|
+
return {
|
|
22
|
+
...original,
|
|
23
|
+
// A plain options object, not `defineComponent`. This factory is hoisted above the
|
|
24
|
+
// imports and runs while `@n8n/frontend-test-utils` is still initialising — it reaches
|
|
25
|
+
// design-system through its renderer — so calling anything imported from `vue` here
|
|
26
|
+
// throws a TDZ error on an import that has not been evaluated yet.
|
|
27
|
+
N8nDataTableServer: {
|
|
28
|
+
props: {
|
|
29
|
+
headers: { type: Array, required: true },
|
|
30
|
+
items: { type: Array, required: true },
|
|
31
|
+
itemsLength: { type: Number, required: true },
|
|
32
|
+
sortBy: { type: Array },
|
|
33
|
+
page: { type: Number },
|
|
34
|
+
itemsPerPage: { type: Number },
|
|
35
|
+
},
|
|
36
|
+
setup(_: unknown, { emit }: { emit: (event: string, ...args: unknown[]) => void }) {
|
|
37
|
+
addEmitter('n8nDataTableServer', emit);
|
|
38
|
+
},
|
|
39
|
+
template: `
|
|
40
|
+
<div data-test-id="insights-table">
|
|
41
|
+
<div class="table-header">
|
|
42
|
+
<div v-for="header in headers" :key="header.key">
|
|
43
|
+
<button
|
|
44
|
+
v-if="!header.disableSort"
|
|
45
|
+
:data-test-id="'sort-' + header.key"
|
|
46
|
+
@click="$emit('update:sortBy', [{ id: header.key, desc: false }])"
|
|
47
|
+
>
|
|
48
|
+
{{ header.title }}
|
|
49
|
+
</button>
|
|
50
|
+
<span v-else :data-test-id="'header-' + header.key">
|
|
51
|
+
{{ header.title }}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div v-for="item in items" :key="item.workflowId"
|
|
56
|
+
:data-test-id="'workflow-row-' + item.workflowId">
|
|
57
|
+
<div v-for="header in headers" :key="header.key">
|
|
58
|
+
<slot :name="'item.' + header.key" :item="item"
|
|
59
|
+
:value="header.value ? header.value(item) : item[header.key]">
|
|
60
|
+
<span v-if="header.value">{{ header.value(item) }}</span>
|
|
61
|
+
<span v-else>{{ item[header.key] }}</span>
|
|
62
|
+
</slot>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<slot name="cover" />
|
|
66
|
+
</div>`,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const mockInsightsData: InsightsByWorkflow = {
|
|
72
|
+
count: 2,
|
|
73
|
+
data: [
|
|
74
|
+
{
|
|
75
|
+
workflowId: 'workflow-1',
|
|
76
|
+
workflowName: 'Test Workflow 1',
|
|
77
|
+
hasReadAccess: true,
|
|
78
|
+
total: 100,
|
|
79
|
+
failed: 5,
|
|
80
|
+
failureRate: 0.05,
|
|
81
|
+
timeSaved: 3600,
|
|
82
|
+
averageRunTime: 1200,
|
|
83
|
+
projectName: 'Test Project 1',
|
|
84
|
+
projectId: 'project-1',
|
|
85
|
+
succeeded: 95,
|
|
86
|
+
runTime: 114000,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
workflowId: 'workflow-2',
|
|
90
|
+
workflowName: 'Test Workflow 2 With Very Long Name That Should Be Truncated',
|
|
91
|
+
hasReadAccess: true,
|
|
92
|
+
total: 50,
|
|
93
|
+
failed: 0,
|
|
94
|
+
failureRate: 0,
|
|
95
|
+
timeSaved: 0,
|
|
96
|
+
averageRunTime: 800,
|
|
97
|
+
projectName: '',
|
|
98
|
+
projectId: 'project-2',
|
|
99
|
+
succeeded: 50,
|
|
100
|
+
runTime: 40000,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
let renderComponent: ReturnType<typeof createComponentRenderer>;
|
|
106
|
+
|
|
107
|
+
describe('InsightsTableWorkflows', () => {
|
|
108
|
+
// The paywall is a `defineAsyncComponent`, and its module pulls `@n8n/stores` in
|
|
109
|
+
// from source. Resolving that inside the assertion made the test a race against a
|
|
110
|
+
// wall clock: ~1.3s locally, over the 3s budget on a CI runner. Loading it here
|
|
111
|
+
// puts the cost in a hook and leaves the loader hitting the module cache, so the
|
|
112
|
+
// assertion no longer depends on how fast the machine is.
|
|
113
|
+
beforeAll(async () => {
|
|
114
|
+
await import('../InsightsPaywall.vue');
|
|
115
|
+
}, 60000);
|
|
116
|
+
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
|
119
|
+
pinia: createTestingPinia(),
|
|
120
|
+
props: {
|
|
121
|
+
data: mockInsightsData,
|
|
122
|
+
loading: false,
|
|
123
|
+
isDashboardEnabled: true, // Default to true for basic tests
|
|
124
|
+
},
|
|
125
|
+
global: {
|
|
126
|
+
stubs: {
|
|
127
|
+
RouterLink: {
|
|
128
|
+
template: '<a><slot /></a>',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
afterEach(() => {
|
|
136
|
+
vi.clearAllMocks();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('basic rendering', () => {
|
|
140
|
+
it('should display the correct heading', () => {
|
|
141
|
+
renderComponent();
|
|
142
|
+
|
|
143
|
+
expect(screen.getByRole('heading', { name: 'Breakdown by workflow' })).toBeInTheDocument();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should render workflow data in table rows', () => {
|
|
147
|
+
renderComponent();
|
|
148
|
+
|
|
149
|
+
expect(screen.getByTestId('workflow-row-workflow-1')).toBeInTheDocument();
|
|
150
|
+
expect(screen.getByTestId('workflow-row-workflow-2')).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('data display', () => {
|
|
155
|
+
it('should display formatted numbers for total and failed columns', () => {
|
|
156
|
+
renderComponent();
|
|
157
|
+
|
|
158
|
+
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
|
159
|
+
expect(within(row1).getByText('100')).toBeInTheDocument();
|
|
160
|
+
expect(within(row1).getByText('5')).toBeInTheDocument();
|
|
161
|
+
|
|
162
|
+
const row2 = screen.getByTestId('workflow-row-workflow-2');
|
|
163
|
+
expect(within(row2).getByText('50')).toBeInTheDocument();
|
|
164
|
+
expect(within(row2).getByText('0')).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle large numbers with locale formatting', () => {
|
|
168
|
+
const largeNumberData = {
|
|
169
|
+
...mockInsightsData,
|
|
170
|
+
data: [
|
|
171
|
+
{
|
|
172
|
+
...mockInsightsData.data[0],
|
|
173
|
+
total: 1000000,
|
|
174
|
+
failed: 50000,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
renderComponent({
|
|
180
|
+
props: {
|
|
181
|
+
data: largeNumberData,
|
|
182
|
+
loading: false,
|
|
183
|
+
isDashboardEnabled: true,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const row = screen.getByTestId('workflow-row-workflow-1');
|
|
188
|
+
expect(within(row).getByText('1,000,000')).toBeInTheDocument();
|
|
189
|
+
expect(within(row).getByText('50,000')).toBeInTheDocument();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('template slots', () => {
|
|
194
|
+
describe('workflowName slot', () => {
|
|
195
|
+
it('should render workflow name with tooltip', () => {
|
|
196
|
+
renderComponent();
|
|
197
|
+
|
|
198
|
+
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
|
199
|
+
expect(within(row1).getByText('Test Workflow 1')).toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should track telemetry on workflow name click', async () => {
|
|
203
|
+
const user = userEvent.setup();
|
|
204
|
+
renderComponent();
|
|
205
|
+
|
|
206
|
+
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
|
207
|
+
const workflowLink = within(row1).getByText('Test Workflow 1');
|
|
208
|
+
await user.click(workflowLink);
|
|
209
|
+
|
|
210
|
+
expect(mockTelemetry.track).toHaveBeenCalledWith(
|
|
211
|
+
'User clicked on workflow from insights table',
|
|
212
|
+
{
|
|
213
|
+
workflow_id: 'workflow-1',
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should render workflow name as a link when the user has access', () => {
|
|
219
|
+
renderComponent();
|
|
220
|
+
|
|
221
|
+
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
|
222
|
+
expect(within(row1).getByText('Test Workflow 1').closest('a')).not.toBeNull();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should render workflow name as static text without a link when the user has no access', () => {
|
|
226
|
+
const noAccessData: InsightsByWorkflow = {
|
|
227
|
+
count: 1,
|
|
228
|
+
data: [{ ...mockInsightsData.data[0], hasReadAccess: false }],
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
renderComponent({
|
|
232
|
+
props: { data: noAccessData, loading: false, isDashboardEnabled: true },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
|
236
|
+
const name = within(row1).getByText('Test Workflow 1');
|
|
237
|
+
expect(name).toBeInTheDocument();
|
|
238
|
+
expect(name.closest('a')).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should not track telemetry when clicking a workflow the user cannot access', async () => {
|
|
242
|
+
const user = userEvent.setup();
|
|
243
|
+
const noAccessData: InsightsByWorkflow = {
|
|
244
|
+
count: 1,
|
|
245
|
+
data: [{ ...mockInsightsData.data[0], hasReadAccess: false }],
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
renderComponent({
|
|
249
|
+
props: { data: noAccessData, loading: false, isDashboardEnabled: true },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
|
253
|
+
await user.click(within(row1).getByText('Test Workflow 1'));
|
|
254
|
+
|
|
255
|
+
expect(mockTelemetry.track).not.toHaveBeenCalledWith(
|
|
256
|
+
'User clicked on workflow from insights table',
|
|
257
|
+
expect.anything(),
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('timeSaved slot', () => {
|
|
263
|
+
it('should show estimate link when timeSaved is 0', () => {
|
|
264
|
+
renderComponent();
|
|
265
|
+
|
|
266
|
+
const row2 = screen.getByTestId('workflow-row-workflow-2');
|
|
267
|
+
expect(within(row2).getByText('Estimate')).toBeInTheDocument();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should show formatted value when timeSaved exists', () => {
|
|
271
|
+
renderComponent();
|
|
272
|
+
|
|
273
|
+
// The actual formatted value will be processed by the utility functions
|
|
274
|
+
// We just verify the slot is rendered with the item that has timeSaved
|
|
275
|
+
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
|
276
|
+
expect(row1).toBeInTheDocument();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('projectName slot', () => {
|
|
281
|
+
it('should render project name with tooltip when projectName exists', () => {
|
|
282
|
+
renderComponent();
|
|
283
|
+
|
|
284
|
+
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
|
285
|
+
expect(within(row1).getByText('Test Project 1')).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should render dash when projectName is null', () => {
|
|
289
|
+
renderComponent();
|
|
290
|
+
|
|
291
|
+
const row2 = screen.getByTestId('workflow-row-workflow-2');
|
|
292
|
+
expect(within(row2).getByText('-')).toBeInTheDocument();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('event delegation', () => {
|
|
298
|
+
it('should delegate update:options event from N8nDataTableServer', () => {
|
|
299
|
+
const { emitted } = renderComponent();
|
|
300
|
+
|
|
301
|
+
const updateOptions = {
|
|
302
|
+
page: 1,
|
|
303
|
+
itemsPerPage: 20,
|
|
304
|
+
sortBy: [{ id: 'workflowName', desc: false }],
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
emitters.n8nDataTableServer.emit('update:options', updateOptions);
|
|
308
|
+
|
|
309
|
+
expect(emitted()).toHaveProperty('update:options');
|
|
310
|
+
expect(emitted()['update:options'][0]).toEqual([updateOptions]);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('telemetry tracking', () => {
|
|
315
|
+
it('should track sort changes with correct payload', async () => {
|
|
316
|
+
const { rerender } = renderComponent();
|
|
317
|
+
|
|
318
|
+
// Simulate sortBy change
|
|
319
|
+
await rerender({
|
|
320
|
+
sortBy: [{ id: 'total', desc: true }],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(mockTelemetry.track).toHaveBeenCalledWith('User sorted insights table', {
|
|
324
|
+
sorted_by: [
|
|
325
|
+
{
|
|
326
|
+
id: 'total',
|
|
327
|
+
desc: true,
|
|
328
|
+
label: 'Prod. executions',
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should handle empty sortBy array', async () => {
|
|
335
|
+
const { rerender } = renderComponent();
|
|
336
|
+
|
|
337
|
+
// Simulate sortBy change to empty array
|
|
338
|
+
await rerender({
|
|
339
|
+
sortBy: [],
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(mockTelemetry.track).toHaveBeenCalledWith('User sorted insights table', {
|
|
343
|
+
sorted_by: [],
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('edge cases', () => {
|
|
349
|
+
it('should handle empty data array', () => {
|
|
350
|
+
const emptyData = { count: 0, data: [] };
|
|
351
|
+
|
|
352
|
+
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
|
353
|
+
pinia: createTestingPinia(),
|
|
354
|
+
props: {
|
|
355
|
+
data: emptyData,
|
|
356
|
+
loading: false,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
renderComponent();
|
|
361
|
+
|
|
362
|
+
expect(screen.getByTestId('insights-table')).toBeInTheDocument();
|
|
363
|
+
expect(screen.queryByTestId('workflow-row-workflow-1')).not.toBeInTheDocument();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should handle loading state', () => {
|
|
367
|
+
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
|
368
|
+
pinia: createTestingPinia(),
|
|
369
|
+
props: {
|
|
370
|
+
data: mockInsightsData,
|
|
371
|
+
loading: true,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
renderComponent();
|
|
376
|
+
|
|
377
|
+
expect(screen.getByTestId('insights-table')).toBeInTheDocument();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should handle missing optional props', () => {
|
|
381
|
+
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
|
382
|
+
pinia: createTestingPinia(),
|
|
383
|
+
props: {
|
|
384
|
+
data: mockInsightsData,
|
|
385
|
+
// loading prop omitted
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
renderComponent();
|
|
390
|
+
|
|
391
|
+
expect(screen.getByTestId('insights-table')).toBeInTheDocument();
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe('paywall functionality', () => {
|
|
396
|
+
it('should not display paywall when dashboard is enabled', () => {
|
|
397
|
+
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
|
398
|
+
pinia: createTestingPinia(),
|
|
399
|
+
props: {
|
|
400
|
+
data: mockInsightsData,
|
|
401
|
+
loading: false,
|
|
402
|
+
isDashboardEnabled: true,
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
renderComponent();
|
|
407
|
+
|
|
408
|
+
expect(
|
|
409
|
+
screen.queryByRole('heading', { name: 'Upgrade to access more detailed insights' }),
|
|
410
|
+
).not.toBeInTheDocument();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should render paywall cover when dashboard is not enabled', async () => {
|
|
414
|
+
const { container } = renderComponent({
|
|
415
|
+
props: {
|
|
416
|
+
data: mockInsightsData,
|
|
417
|
+
loading: false,
|
|
418
|
+
isDashboardEnabled: false,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Verify sample data is used instead of real data
|
|
423
|
+
expect(screen.getByTestId('workflow-row-sample-workflow-1')).toBeInTheDocument();
|
|
424
|
+
expect(screen.queryByTestId('workflow-row-workflow-1')).not.toBeInTheDocument();
|
|
425
|
+
|
|
426
|
+
// Wait for the blurryCover div that contains the paywall to be present
|
|
427
|
+
await screen.findByTestId('insights-table');
|
|
428
|
+
const blurryCover = container.querySelector('[class*="blurryCover"]');
|
|
429
|
+
expect(blurryCover).toBeInTheDocument();
|
|
430
|
+
|
|
431
|
+
// Wait for the async paywall component to load and render
|
|
432
|
+
// The paywall text should appear once the component loads
|
|
433
|
+
// The module is already loaded (see `beforeAll`), so this waits only for the
|
|
434
|
+
// async component to swap in — a tick, not a transform.
|
|
435
|
+
const paywallText = await screen.findByText('Upgrade to access more detailed insights');
|
|
436
|
+
expect(paywallText).toBeInTheDocument();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should use sample data when dashboard is not enabled', () => {
|
|
440
|
+
renderComponent({
|
|
441
|
+
props: {
|
|
442
|
+
data: mockInsightsData,
|
|
443
|
+
loading: false,
|
|
444
|
+
isDashboardEnabled: false,
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Should render sample workflows instead of actual data
|
|
449
|
+
expect(screen.getByTestId('workflow-row-sample-workflow-1')).toBeInTheDocument();
|
|
450
|
+
expect(screen.getByTestId('workflow-row-sample-workflow-2')).toBeInTheDocument();
|
|
451
|
+
// Should not render the original test data
|
|
452
|
+
expect(screen.queryByTestId('workflow-row-workflow-1')).not.toBeInTheDocument();
|
|
453
|
+
expect(screen.queryByTestId('workflow-row-workflow-2')).not.toBeInTheDocument();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should disable sorting when dashboard is not enabled', () => {
|
|
457
|
+
renderComponent({
|
|
458
|
+
props: {
|
|
459
|
+
data: mockInsightsData,
|
|
460
|
+
loading: false,
|
|
461
|
+
isDashboardEnabled: false,
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// When sorting is disabled, columns should not have clickable sort buttons
|
|
466
|
+
expect(screen.queryByTestId('sort-workflowName')).not.toBeInTheDocument();
|
|
467
|
+
expect(screen.queryByTestId('sort-total')).not.toBeInTheDocument();
|
|
468
|
+
expect(screen.queryByTestId('sort-failed')).not.toBeInTheDocument();
|
|
469
|
+
expect(screen.queryByTestId('sort-failureRate')).not.toBeInTheDocument();
|
|
470
|
+
expect(screen.queryByTestId('sort-timeSaved')).not.toBeInTheDocument();
|
|
471
|
+
expect(screen.queryByTestId('sort-averageRunTime')).not.toBeInTheDocument();
|
|
472
|
+
|
|
473
|
+
// Headers should be present as non-clickable elements
|
|
474
|
+
expect(screen.getByTestId('header-workflowName')).toBeInTheDocument();
|
|
475
|
+
expect(screen.getByTestId('header-total')).toBeInTheDocument();
|
|
476
|
+
expect(screen.getByTestId('header-failed')).toBeInTheDocument();
|
|
477
|
+
expect(screen.getByTestId('header-failureRate')).toBeInTheDocument();
|
|
478
|
+
expect(screen.getByTestId('header-timeSaved')).toBeInTheDocument();
|
|
479
|
+
expect(screen.getByTestId('header-averageRunTime')).toBeInTheDocument();
|
|
480
|
+
// projectName is always disabled
|
|
481
|
+
expect(screen.getByTestId('header-projectName')).toBeInTheDocument();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should enable sorting when dashboard is enabled', () => {
|
|
485
|
+
renderComponent();
|
|
486
|
+
|
|
487
|
+
// When sorting is enabled, most columns should have clickable sort buttons
|
|
488
|
+
expect(screen.getByTestId('sort-workflowName')).toBeInTheDocument();
|
|
489
|
+
expect(screen.getByTestId('sort-total')).toBeInTheDocument();
|
|
490
|
+
expect(screen.getByTestId('sort-failed')).toBeInTheDocument();
|
|
491
|
+
expect(screen.getByTestId('sort-failureRate')).toBeInTheDocument();
|
|
492
|
+
expect(screen.getByTestId('sort-timeSaved')).toBeInTheDocument();
|
|
493
|
+
expect(screen.getByTestId('sort-averageRunTime')).toBeInTheDocument();
|
|
494
|
+
|
|
495
|
+
// projectName is always disabled (non-clickable)
|
|
496
|
+
expect(screen.getByTestId('header-projectName')).toBeInTheDocument();
|
|
497
|
+
expect(screen.queryByTestId('sort-projectName')).not.toBeInTheDocument();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should trigger sort when clicking on sortable column header', async () => {
|
|
501
|
+
const { emitted } = renderComponent();
|
|
502
|
+
|
|
503
|
+
const sortButton = screen.getByTestId('sort-workflowName');
|
|
504
|
+
await userEvent.click(sortButton);
|
|
505
|
+
|
|
506
|
+
expect(emitted()['update:sortBy']).toEqual([[[{ id: 'workflowName', desc: false }]]]);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|