|
8 | 8 | createTimeEntryViaApi, |
9 | 9 | createTimeEntryWithTagViaApi, |
10 | 10 | createBareTimeEntryViaApi, |
| 11 | + createBillableProjectViaApi, |
11 | 12 | } from './utils/api'; |
12 | 13 | import { |
13 | 14 | goToReporting, |
@@ -577,3 +578,58 @@ test('test that updating expiration date on already-public report works', async |
577 | 578 | const now = new Date(); |
578 | 579 | expect(returnedDate.getTime()).toBeGreaterThan(now.getTime()); |
579 | 580 | }); |
| 581 | + |
| 582 | +// ────────────────────────────────────────────────── |
| 583 | +// Shared Report Cost Column Tests |
| 584 | +// ────────────────────────────────────────────────── |
| 585 | + |
| 586 | +test('test that shared report displays cost column correctly aligned with data rows', async ({ |
| 587 | + page, |
| 588 | + ctx, |
| 589 | +}) => { |
| 590 | + const projectName = 'BillableProj ' + Math.floor(Math.random() * 10000); |
| 591 | + const reportName = 'BillableReport ' + Math.floor(Math.random() * 10000); |
| 592 | + |
| 593 | + const project = await createBillableProjectViaApi(ctx, { |
| 594 | + name: projectName, |
| 595 | + billable_rate: 10000, // 100.00 per hour |
| 596 | + }); |
| 597 | + await createTimeEntryViaApi(ctx, { |
| 598 | + description: `Entry for ${projectName}`, |
| 599 | + duration: '1h', |
| 600 | + projectId: project.id, |
| 601 | + billable: true, |
| 602 | + }); |
| 603 | + |
| 604 | + await goToReporting(page); |
| 605 | + await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible(); |
| 606 | + |
| 607 | + const { shareableLink } = await saveAsSharedReport(page, reportName); |
| 608 | + |
| 609 | + // Navigate to the shared report |
| 610 | + await page.goto(shareableLink); |
| 611 | + await expect(page.getByText('Reporting')).toBeVisible(); |
| 612 | + await expect(page.getByText(projectName)).toBeVisible(); |
| 613 | + |
| 614 | + // Verify the table header has all three columns |
| 615 | + await expect(page.getByText('Name', { exact: true })).toBeVisible(); |
| 616 | + await expect(page.getByText('Duration', { exact: true })).toBeVisible(); |
| 617 | + await expect(page.getByText('Cost', { exact: true })).toBeVisible(); |
| 618 | + |
| 619 | + // Verify the Total row displays both duration and cost |
| 620 | + await expect(page.getByText('Total')).toBeVisible(); |
| 621 | + |
| 622 | + // The data rows should render cost values (not just header + duration) |
| 623 | + // With 1h at 100/h the cost should be displayed somewhere in the table |
| 624 | + // If showCost is not passed to ReportingRow, only the header "Cost" and |
| 625 | + // the Total row cost will render, but individual row costs will be missing |
| 626 | + const table = page.locator('[style*="grid-template-columns"]'); |
| 627 | + // Count elements containing the cost value - header "Cost" + project row cost + total row cost = 3 |
| 628 | + // If broken (showCost not passed), the project row won't render its cost cell |
| 629 | + await expect(table.getByText(/100/).first()).toBeVisible(); |
| 630 | + |
| 631 | + // Verify the cost value appears at least twice in the table |
| 632 | + // (once for the data row, once for the total) beyond just the header |
| 633 | + const costValues = table.getByText(/100/); |
| 634 | + await expect(costValues).toHaveCount(2); |
| 635 | +}); |
0 commit comments