|
|
|
|
@@ -1,5 +1,186 @@
|
|
|
|
|
// TODO: Test filter
|
|
|
|
|
import { expect, Page } from '@playwright/test';
|
|
|
|
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
|
|
|
|
import { test } from '../playwright/fixtures';
|
|
|
|
|
|
|
|
|
|
// TODO: Test date range
|
|
|
|
|
|
|
|
|
|
// TODO: Test grouping and sub-grouping
|
|
|
|
|
|
|
|
|
|
async function goToTimeOverview(page: Page) {
|
|
|
|
|
await page.goto(PLAYWRIGHT_BASE_URL + '/time');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function goToReporting(page: Page) {
|
|
|
|
|
await page.goto(PLAYWRIGHT_BASE_URL + '/reporting');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function goToReportingDetailed(page: Page) {
|
|
|
|
|
await page.goto(PLAYWRIGHT_BASE_URL + '/reporting/detailed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createTimeEntryWithProject(page: Page, projectName: string, duration: string) {
|
|
|
|
|
// First create the project through the Projects page
|
|
|
|
|
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
|
|
|
|
|
await page.getByRole('button', { name: 'Create Project' }).click();
|
|
|
|
|
await page.getByLabel('Project Name').fill(projectName);
|
|
|
|
|
await page.getByRole('dialog').getByRole('button', { name: 'Create Project' }).click();
|
|
|
|
|
|
|
|
|
|
// Wait for the project to be created and visible in the list
|
|
|
|
|
await page.getByText(projectName).waitFor({ state: 'visible' });
|
|
|
|
|
|
|
|
|
|
// Then create the time entry
|
|
|
|
|
await goToTimeOverview(page);
|
|
|
|
|
await page.getByRole('button', { name: 'Manual time entry' }).click();
|
|
|
|
|
|
|
|
|
|
// Fill in the time entry details
|
|
|
|
|
await page.getByTestId('time_entry_description').fill(`Time entry for ${projectName}`);
|
|
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: 'No Project' }).click();
|
|
|
|
|
await page.getByText(projectName).click();
|
|
|
|
|
|
|
|
|
|
// Set duration
|
|
|
|
|
await page.locator('[role="dialog"] input[name="Duration"]').fill(duration);
|
|
|
|
|
await page.locator('[role="dialog"] input[name="Duration"]').press('Tab');
|
|
|
|
|
|
|
|
|
|
// Submit the time entry
|
|
|
|
|
await Promise.all([
|
|
|
|
|
page.getByRole('button', { name: 'Create Time Entry' }).click(),
|
|
|
|
|
page.waitForLoadState('networkidle')
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createTimeEntryWithTag(page: Page, tagName: string, duration: string) {
|
|
|
|
|
await goToTimeOverview(page);
|
|
|
|
|
await page.getByRole('button', { name: 'Manual time entry' }).click();
|
|
|
|
|
|
|
|
|
|
// Fill in the time entry details
|
|
|
|
|
await page.getByTestId('time_entry_description').fill(`Time entry with tag ${tagName}`);
|
|
|
|
|
|
|
|
|
|
// Add tag
|
|
|
|
|
await page.getByRole('button', { name: 'Tags' }).click();
|
|
|
|
|
await page.getByText('Create new tag').click();
|
|
|
|
|
await page.getByPlaceholder('Tag Name').fill(tagName);
|
|
|
|
|
await page.getByRole('button', { name: 'Create Tag' }).click();
|
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
|
|
|
|
|
|
// Set duration
|
|
|
|
|
await page.locator('[role="dialog"] input[name="Duration"]').fill(duration);
|
|
|
|
|
await page.locator('[role="dialog"] input[name="Duration"]').press('Tab');
|
|
|
|
|
|
|
|
|
|
// Submit the time entry
|
|
|
|
|
await page.getByRole('button', { name: 'Create Time Entry' }).click();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createTimeEntryWithBillableStatus(page: Page, isBillable: boolean, duration: string) {
|
|
|
|
|
await goToTimeOverview(page);
|
|
|
|
|
await page.getByRole('button', { name: 'Manual time entry' }).click();
|
|
|
|
|
|
|
|
|
|
// Fill in the time entry details
|
|
|
|
|
await page.getByTestId('time_entry_description').fill(`Time entry ${isBillable ? 'billable' : 'non-billable'}`);
|
|
|
|
|
|
|
|
|
|
// Set billable status
|
|
|
|
|
await page.getByRole('button', { name: 'Non-Billable' }).click();
|
|
|
|
|
if (!isBillable) {
|
|
|
|
|
await page.getByRole('option', { name: 'Non Billable', exact: true }).click();
|
|
|
|
|
} else {
|
|
|
|
|
await page.getByRole('option', { name: 'Billable', exact: true }).click();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set duration
|
|
|
|
|
await page.locator('[role="dialog"] input[name="Duration"]').fill(duration);
|
|
|
|
|
await page.locator('[role="dialog"] input[name="Duration"]').press('Tab');
|
|
|
|
|
|
|
|
|
|
// Submit the time entry
|
|
|
|
|
await page.getByRole('button', { name: 'Create Time Entry' }).click();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('test that project filtering works in reporting', async ({ page }) => {
|
|
|
|
|
const project1 = 'Test Project 1 ' + Math.floor(Math.random() * 10000);
|
|
|
|
|
const project2 = 'Test Project 2 ' + Math.floor(Math.random() * 10000);
|
|
|
|
|
|
|
|
|
|
// Create time entries for both projects
|
|
|
|
|
await createTimeEntryWithProject(page, project1, '1h');
|
|
|
|
|
await createTimeEntryWithProject(page, project2, '2h');
|
|
|
|
|
|
|
|
|
|
// Go to reporting and filter by project1
|
|
|
|
|
await goToReporting(page);
|
|
|
|
|
await page.getByRole('button', { name: 'Project' }).nth(0).click();
|
|
|
|
|
await page.getByText(project1).click();
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
// escape
|
|
|
|
|
page.keyboard.press('Escape'),
|
|
|
|
|
// wait for API request to finish
|
|
|
|
|
page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200)
|
|
|
|
|
]);
|
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
|
|
|
|
|
|
// Verify only project1 time entries are shown
|
|
|
|
|
await expect(page.getByText(project1)).toBeVisible();
|
|
|
|
|
await expect(page.getByText(project2)).not.toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('test that tag filtering works in reporting', async ({ page }) => {
|
|
|
|
|
const tag1 = 'Test Tag 1 ' + Math.floor(Math.random() * 10000);
|
|
|
|
|
const tag2 = 'Test Tag 2 ' + Math.floor(Math.random() * 10000);
|
|
|
|
|
|
|
|
|
|
// Create time entries with different tags
|
|
|
|
|
await createTimeEntryWithTag(page, tag1, '1h');
|
|
|
|
|
await createTimeEntryWithTag(page, tag2, '2h');
|
|
|
|
|
|
|
|
|
|
// Go to reporting and filter by tag1
|
|
|
|
|
await goToReporting(page);
|
|
|
|
|
// wait for all requests to finish
|
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: 'Tags' }).click();
|
|
|
|
|
await page.getByText(tag1).click();
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
// escape
|
|
|
|
|
page.keyboard.press('Escape'),
|
|
|
|
|
// wait for API request to finish
|
|
|
|
|
page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200)
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Verify only time entries with tag1 are shown
|
|
|
|
|
await expect(page.getByText('1h 00min').first()).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('test that billable status filtering works in reporting', async ({ page }) => {
|
|
|
|
|
// Create billable and non-billable time entries
|
|
|
|
|
await createTimeEntryWithBillableStatus(page, true, '1h');
|
|
|
|
|
await createTimeEntryWithBillableStatus(page, false, '2h');
|
|
|
|
|
|
|
|
|
|
// Go to reporting and filter by billable
|
|
|
|
|
await goToReporting(page);
|
|
|
|
|
|
|
|
|
|
await page.getByRole('button', { name: 'Billable' }).click();
|
|
|
|
|
await page.getByRole('option', { name: 'Billable', exact: true }).click();
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
// escape
|
|
|
|
|
page.keyboard.press('Escape'),
|
|
|
|
|
// wait for API request to finish
|
|
|
|
|
page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200)
|
|
|
|
|
]);
|
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
|
|
|
|
|
|
await expect(page.getByText('1h 00min').first()).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('test that detailed view shows time entries correctly', async ({ page }) => {
|
|
|
|
|
const projectName = 'Detailed View Project ' + Math.floor(Math.random() * 10000);
|
|
|
|
|
|
|
|
|
|
// Create a time entry
|
|
|
|
|
await createTimeEntryWithProject(page, projectName, '1h');
|
|
|
|
|
|
|
|
|
|
// Go to detailed reporting view
|
|
|
|
|
await goToReportingDetailed(page);
|
|
|
|
|
|
|
|
|
|
// Verify the time entry is shown with all details
|
|
|
|
|
await expect(page.getByText(projectName, { exact: true })).toBeVisible();
|
|
|
|
|
await expect(page.locator('input[name="Duration"]')).toHaveValue('1h 00min');
|
|
|
|
|
await expect(page.getByText('Time entry for ' + projectName, { exact: true })).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// TODO: test that date range filtering works in reporting
|
|
|
|
|
|