Skip to content

Commit 147514a

Browse files
committed
convert billable query string to boolean for shared report + e2e tests #876
1 parent 435522b commit 147514a

File tree

2 files changed

+198
-1
lines changed

2 files changed

+198
-1
lines changed

e2e/shared-reports.spec.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
createTimeEntryWithTagViaApi,
1010
createBareTimeEntryViaApi,
1111
createBillableProjectViaApi,
12+
createTimeEntryWithBillableStatusViaApi,
13+
createTagViaApi,
1214
} from './utils/api';
1315
import {
1416
goToReporting,
@@ -247,6 +249,191 @@ test('test that shared report with No Task filter shows entries without a task',
247249
await expect(page.getByText('Total')).toBeVisible();
248250
});
249251

252+
test('test that shared report respects task filter', async ({ page, ctx }) => {
253+
const projectName = 'TaskFilterProj ' + Math.floor(Math.random() * 10000);
254+
const taskA = 'TaskA ' + Math.floor(Math.random() * 10000);
255+
const taskB = 'TaskB ' + Math.floor(Math.random() * 10000);
256+
const reportName = 'TaskFilterReport ' + Math.floor(Math.random() * 10000);
257+
258+
const project = await createProjectViaApi(ctx, { name: projectName });
259+
const task = await createTaskViaApi(ctx, { name: taskA, project_id: project.id });
260+
await createTaskViaApi(ctx, { name: taskB, project_id: project.id });
261+
await createTimeEntryViaApi(ctx, {
262+
description: `Entry for ${taskA}`,
263+
duration: '1h',
264+
projectId: project.id,
265+
taskId: task.id,
266+
});
267+
await createTimeEntryViaApi(ctx, {
268+
description: `Entry for ${projectName} no task`,
269+
duration: '2h',
270+
projectId: project.id,
271+
});
272+
273+
await goToReporting(page);
274+
await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();
275+
276+
// Filter by task A
277+
await page.getByRole('button', { name: 'Tasks' }).first().click();
278+
await Promise.all([
279+
page.getByRole('option').filter({ hasText: taskA }).click(),
280+
waitForReportingUpdate(page),
281+
]);
282+
await page.keyboard.press('Escape');
283+
284+
const { shareableLink } = await saveAsSharedReport(page, reportName);
285+
286+
// View the shared report
287+
await page.goto(shareableLink);
288+
await expect(page.getByText('Reporting')).toBeVisible();
289+
await expect(page.getByText('Total')).toBeVisible();
290+
await expect(page.getByText('1h 00min').first()).toBeVisible();
291+
await expect(page.getByText('3h 00min')).not.toBeVisible();
292+
});
293+
294+
test('test that shared report respects client filter', async ({ page, ctx }) => {
295+
const clientA = 'ClientA ' + Math.floor(Math.random() * 10000);
296+
const clientB = 'ClientB ' + Math.floor(Math.random() * 10000);
297+
const projectA = 'ClientFilterProjA ' + Math.floor(Math.random() * 10000);
298+
const projectB = 'ClientFilterProjB ' + Math.floor(Math.random() * 10000);
299+
const reportName = 'ClientFilterReport ' + Math.floor(Math.random() * 10000);
300+
301+
const cliA = await createClientViaApi(ctx, { name: clientA });
302+
const cliB = await createClientViaApi(ctx, { name: clientB });
303+
const projA = await createProjectViaApi(ctx, { name: projectA, client_id: cliA.id });
304+
const projB = await createProjectViaApi(ctx, { name: projectB, client_id: cliB.id });
305+
await createTimeEntryViaApi(ctx, {
306+
description: `Entry for ${clientA}`,
307+
duration: '1h',
308+
projectId: projA.id,
309+
});
310+
await createTimeEntryViaApi(ctx, {
311+
description: `Entry for ${clientB}`,
312+
duration: '2h',
313+
projectId: projB.id,
314+
});
315+
316+
await goToReporting(page);
317+
await expect(page.getByTestId('reporting_view').getByText(projectA)).toBeVisible();
318+
319+
// Filter by client A
320+
await page.getByRole('button', { name: 'Clients' }).first().click();
321+
await Promise.all([
322+
page.getByRole('option').filter({ hasText: clientA }).click(),
323+
waitForReportingUpdate(page),
324+
]);
325+
await page.keyboard.press('Escape');
326+
327+
const { shareableLink } = await saveAsSharedReport(page, reportName);
328+
329+
// View the shared report
330+
await page.goto(shareableLink);
331+
await expect(page.getByText('Reporting')).toBeVisible();
332+
await expect(page.getByText(projectA)).toBeVisible();
333+
await expect(page.getByText(projectB)).not.toBeVisible();
334+
});
335+
336+
test('test that shared report respects tag filter', async ({ page, ctx }) => {
337+
const tagA = 'TagA ' + Math.floor(Math.random() * 10000);
338+
const tagB = 'TagB ' + Math.floor(Math.random() * 10000);
339+
const reportName = 'TagFilterReport ' + Math.floor(Math.random() * 10000);
340+
341+
const tagObjA = await createTagViaApi(ctx, { name: tagA });
342+
await createTagViaApi(ctx, { name: tagB });
343+
await createTimeEntryViaApi(ctx, {
344+
description: `Entry with ${tagA}`,
345+
duration: '1h',
346+
tags: [tagObjA.id],
347+
});
348+
await createBareTimeEntryViaApi(ctx, 'Entry no tags', '2h');
349+
350+
await goToReporting(page);
351+
await expect(page.getByTestId('reporting_view').getByText('Total')).toBeVisible();
352+
353+
// Filter by tag A
354+
await page.getByRole('button', { name: 'Tags' }).first().click();
355+
await Promise.all([
356+
page.getByRole('option').filter({ hasText: tagA }).click(),
357+
waitForReportingUpdate(page),
358+
]);
359+
await page.keyboard.press('Escape');
360+
361+
const { shareableLink } = await saveAsSharedReport(page, reportName);
362+
363+
// View the shared report
364+
await page.goto(shareableLink);
365+
await expect(page.getByText('Reporting')).toBeVisible();
366+
await expect(page.getByText('Total')).toBeVisible();
367+
await expect(page.getByText('1h 00min').first()).toBeVisible();
368+
await expect(page.getByText('3h 00min')).not.toBeVisible();
369+
});
370+
371+
test('test that shared report respects member filter', async ({ page, ctx }) => {
372+
const projectName = 'MemberFilterProj ' + Math.floor(Math.random() * 10000);
373+
const reportName = 'MemberFilterReport ' + Math.floor(Math.random() * 10000);
374+
375+
const project = await createProjectViaApi(ctx, { name: projectName });
376+
await createTimeEntryViaApi(ctx, {
377+
description: `Entry for ${projectName}`,
378+
duration: '1h',
379+
projectId: project.id,
380+
});
381+
382+
await goToReporting(page);
383+
await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible();
384+
385+
// Filter by current member (John Doe)
386+
await page.getByRole('button', { name: 'Members' }).first().click();
387+
await Promise.all([
388+
page.getByRole('option').filter({ hasText: 'John Doe' }).click(),
389+
waitForReportingUpdate(page),
390+
]);
391+
await page.keyboard.press('Escape');
392+
393+
const { shareableLink } = await saveAsSharedReport(page, reportName);
394+
395+
// View the shared report — should still show data since all entries belong to this member
396+
await page.goto(shareableLink);
397+
await expect(page.getByText('Reporting')).toBeVisible();
398+
await expect(page.getByText(projectName)).toBeVisible();
399+
await expect(page.getByText('Total')).toBeVisible();
400+
});
401+
402+
test('test that shared report with billable filter only shows billable entries', async ({
403+
page,
404+
ctx,
405+
}) => {
406+
const reportName = 'BillableFilterReport ' + Math.floor(Math.random() * 10000);
407+
408+
// Create one billable (1h) and one non-billable (2h) entry
409+
await createTimeEntryWithBillableStatusViaApi(ctx, true, '1h');
410+
await createTimeEntryWithBillableStatusViaApi(ctx, false, '2h');
411+
412+
await goToReporting(page);
413+
await expect(page.getByTestId('reporting_view').getByText('Total')).toBeVisible();
414+
415+
// Filter by billable only
416+
await page.getByRole('combobox').filter({ hasText: 'Billable' }).click();
417+
await Promise.all([
418+
page.getByRole('option', { name: 'Billable', exact: true }).click(),
419+
waitForReportingUpdate(page),
420+
]);
421+
422+
// Verify only 1h shows before saving
423+
await expect(page.getByTestId('reporting_view').getByText('1h 00min').first()).toBeVisible();
424+
425+
const { shareableLink } = await saveAsSharedReport(page, reportName);
426+
427+
// Navigate to the shared report
428+
await page.goto(shareableLink);
429+
await expect(page.getByText('Reporting')).toBeVisible();
430+
await expect(page.getByText('Total')).toBeVisible();
431+
432+
// Shared report should only show the 1h billable entry, not the 2h non-billable
433+
await expect(page.getByText('1h 00min').first()).toBeVisible();
434+
await expect(page.getByText('3h 00min')).not.toBeVisible();
435+
});
436+
250437
// ──────────────────────────────────────────────────
251438
// Report Date Picker Tests
252439
// ──────────────────────────────────────────────────

resources/js/Components/Common/Reporting/ReportingOverview.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,18 @@ const aggregatedTableTimeEntries = computed<AggregatedTimeEntries | undefined>((
157157
});
158158
159159
const reportProperties = computed(() => {
160+
const { billable: billableFilter, ...rest } = filterParams.value;
161+
162+
let billableValue: boolean | null = null;
163+
if (billableFilter === 'true') {
164+
billableValue = true;
165+
} else if (billableFilter === 'false') {
166+
billableValue = false;
167+
}
168+
160169
return {
161-
...filterParams.value,
170+
...rest,
171+
billable: billableValue,
162172
group: group.value,
163173
sub_group: subGroup.value,
164174
history_group: getOptimalGroupingOption(startDate.value, endDate.value),

0 commit comments

Comments
 (0)