mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 11:12:16 +01:00
Conditionally show cost column in report tables; Task/Project Modal
Field cleanup; improve estimated time UX
This commit is contained in:
@@ -8,7 +8,12 @@ import {
|
||||
startOrStopTimerWithButton,
|
||||
stoppedTimeEntryResponse,
|
||||
} from './utils/currentTimeEntry';
|
||||
import { createBareTimeEntryViaApi } from './utils/api';
|
||||
import {
|
||||
createBareTimeEntryViaApi,
|
||||
createPublicProjectViaApi,
|
||||
createTimeEntryViaApi,
|
||||
updateOrganizationSettingViaApi,
|
||||
} from './utils/api';
|
||||
|
||||
async function goToDashboard(page: Page) {
|
||||
await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
|
||||
@@ -116,4 +121,70 @@ test.describe('Employee Dashboard Restrictions', () => {
|
||||
// Team Activity should NOT be visible for employees
|
||||
await expect(employee.page.getByText('Team Activity', { exact: true })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('employee cannot see Cost column in This Week table by default', async ({
|
||||
ctx,
|
||||
employee,
|
||||
}) => {
|
||||
const project = await createPublicProjectViaApi(ctx, {
|
||||
name: 'EmpDashBillProj',
|
||||
is_billable: true,
|
||||
billable_rate: 10000,
|
||||
});
|
||||
await createTimeEntryViaApi(
|
||||
{ ...ctx, memberId: employee.memberId },
|
||||
{
|
||||
description: 'Emp dashboard cost entry',
|
||||
duration: '1h',
|
||||
projectId: project.id,
|
||||
billable: true,
|
||||
}
|
||||
);
|
||||
|
||||
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
|
||||
await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// This Week table should be visible
|
||||
await expect(employee.page.getByText('This Week', { exact: true })).toBeVisible();
|
||||
|
||||
// Duration column should be visible, but Cost column should NOT
|
||||
await expect(employee.page.getByText('Duration', { exact: true })).toBeVisible();
|
||||
await expect(employee.page.getByText('Cost', { exact: true })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('employee can see Cost column in This Week table when employees_can_see_billable_rates is enabled', async ({
|
||||
ctx,
|
||||
employee,
|
||||
}) => {
|
||||
await updateOrganizationSettingViaApi(ctx, { employees_can_see_billable_rates: true });
|
||||
|
||||
const project = await createPublicProjectViaApi(ctx, {
|
||||
name: 'EmpDashBillVisProj',
|
||||
is_billable: true,
|
||||
billable_rate: 10000,
|
||||
});
|
||||
await createTimeEntryViaApi(
|
||||
{ ...ctx, memberId: employee.memberId },
|
||||
{
|
||||
description: 'Emp dashboard cost visible entry',
|
||||
duration: '1h',
|
||||
projectId: project.id,
|
||||
billable: true,
|
||||
}
|
||||
);
|
||||
|
||||
await employee.page.goto(PLAYWRIGHT_BASE_URL + '/dashboard');
|
||||
await expect(employee.page.getByTestId('dashboard_view')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Both Duration and Cost columns should be visible
|
||||
await expect(employee.page.getByText('Duration', { exact: true })).toBeVisible();
|
||||
await expect(employee.page.getByText('Cost', { exact: true })).toBeVisible();
|
||||
|
||||
// 1h at 100.00/h = 100.00 EUR cost should be visible
|
||||
await expect(employee.page.getByText('100,00 EUR').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,8 +124,15 @@ test('test that updating billable rate works with existing time entries', async
|
||||
|
||||
await page.getByRole('row').first().getByRole('button').click();
|
||||
await page.getByRole('menuitem').getByText('Edit').first().click();
|
||||
await page.getByText('Non-Billable').click();
|
||||
await page.getByText('Custom Rate').click();
|
||||
|
||||
// Set billable default to Billable
|
||||
await page.getByRole('dialog').locator('#billable').click();
|
||||
await page.getByRole('option', { name: 'Billable', exact: true }).click();
|
||||
|
||||
// Set billable rate to Custom Rate
|
||||
await page.getByRole('dialog').locator('#billableRateType').click();
|
||||
await page.getByRole('option', { name: 'Custom Rate' }).click();
|
||||
|
||||
await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString());
|
||||
await page.getByRole('button', { name: 'Update Project' }).click();
|
||||
|
||||
@@ -153,6 +160,180 @@ test('test that updating billable rate works with existing time entries', async
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('test that creating a project with default billable rate works', async ({ page }) => {
|
||||
const newProjectName = 'Default Rate Project ' + Math.floor(1 + Math.random() * 10000);
|
||||
await goToProjectsOverview(page);
|
||||
await page.getByRole('button', { name: 'Create Project' }).click();
|
||||
await page.getByLabel('Project Name').fill(newProjectName);
|
||||
|
||||
// Set billable default to Billable (leaves rate type as Default Rate)
|
||||
await page.getByRole('dialog').locator('#billable').click();
|
||||
await page.getByRole('option', { name: 'Billable', exact: true }).click();
|
||||
|
||||
// Verify rate type is "Default Rate" and the rate input is disabled
|
||||
await expect(page.getByRole('dialog').locator('#billableRateType')).toContainText(
|
||||
'Default Rate'
|
||||
);
|
||||
await expect(page.getByPlaceholder('Billable Rate')).toBeDisabled();
|
||||
|
||||
await Promise.all([
|
||||
page.getByRole('button', { name: 'Create Project' }).click(),
|
||||
page.waitForResponse(
|
||||
async (response) =>
|
||||
response.url().includes('/projects') &&
|
||||
response.request().method() === 'POST' &&
|
||||
response.status() === 201 &&
|
||||
(await response.json()).data.is_billable === true &&
|
||||
(await response.json()).data.billable_rate === null
|
||||
),
|
||||
]);
|
||||
|
||||
await expect(page.getByTestId('project_table')).toContainText(newProjectName);
|
||||
});
|
||||
|
||||
test('test that creating a non-billable project works', async ({ page }) => {
|
||||
const newProjectName = 'Non-Billable Project ' + Math.floor(1 + Math.random() * 10000);
|
||||
await goToProjectsOverview(page);
|
||||
await page.getByRole('button', { name: 'Create Project' }).click();
|
||||
await page.getByLabel('Project Name').fill(newProjectName);
|
||||
|
||||
// Billable default should already be "Non-billable" by default
|
||||
await expect(page.getByRole('dialog').locator('#billable')).toContainText('Non-billable');
|
||||
|
||||
await Promise.all([
|
||||
page.getByRole('button', { name: 'Create Project' }).click(),
|
||||
page.waitForResponse(
|
||||
async (response) =>
|
||||
response.url().includes('/projects') &&
|
||||
response.request().method() === 'POST' &&
|
||||
response.status() === 201 &&
|
||||
(await response.json()).data.is_billable === false &&
|
||||
(await response.json()).data.billable_rate === null
|
||||
),
|
||||
]);
|
||||
|
||||
await expect(page.getByTestId('project_table')).toContainText(newProjectName);
|
||||
});
|
||||
|
||||
test('test that switching from custom rate to default rate clears billable rate', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
const newProjectName = 'Rate Switch Project ' + Math.floor(1 + Math.random() * 10000);
|
||||
// Create a project with an existing custom billable rate
|
||||
await createProjectViaApi(ctx, {
|
||||
name: newProjectName,
|
||||
is_billable: true,
|
||||
billable_rate: 15000,
|
||||
});
|
||||
|
||||
await goToProjectsOverview(page);
|
||||
await expect(page.getByText(newProjectName)).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.getByRole('row').first().getByRole('button').click();
|
||||
await page.getByRole('menuitem').getByText('Edit').first().click();
|
||||
|
||||
// Verify it loaded as Billable with Custom Rate
|
||||
await expect(page.getByRole('dialog').locator('#billable')).toContainText('Billable');
|
||||
await expect(page.getByRole('dialog').locator('#billableRateType')).toContainText(
|
||||
'Custom Rate'
|
||||
);
|
||||
|
||||
// Switch to Default Rate
|
||||
await page.getByRole('dialog').locator('#billableRateType').click();
|
||||
await page.getByRole('option', { name: 'Default Rate' }).click();
|
||||
|
||||
// Rate input should now be disabled
|
||||
await expect(page.getByPlaceholder('Billable Rate')).toBeDisabled();
|
||||
|
||||
// Submit — billable_rate changes from 15000 to null, so confirmation dialog appears
|
||||
await page.getByRole('button', { name: 'Update Project' }).click();
|
||||
await Promise.all([
|
||||
page.locator('button').filter({ hasText: 'Yes, update existing time' }).click(),
|
||||
page.waitForResponse(
|
||||
async (response) =>
|
||||
response.url().includes('/projects/') &&
|
||||
response.request().method() === 'PUT' &&
|
||||
response.status() === 200 &&
|
||||
(await response.json()).data.is_billable === true &&
|
||||
(await response.json()).data.billable_rate === null
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('test that switching from billable to non-billable preserves rate settings', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
const newProjectName = 'Billable Reset Project ' + Math.floor(1 + Math.random() * 10000);
|
||||
// Create a project with a custom billable rate
|
||||
await createProjectViaApi(ctx, {
|
||||
name: newProjectName,
|
||||
is_billable: true,
|
||||
billable_rate: 20000,
|
||||
});
|
||||
|
||||
await goToProjectsOverview(page);
|
||||
await expect(page.getByText(newProjectName)).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.getByRole('row').first().getByRole('button').click();
|
||||
await page.getByRole('menuitem').getByText('Edit').first().click();
|
||||
|
||||
// Verify it loaded correctly as Billable with Custom Rate
|
||||
await expect(page.getByRole('dialog').locator('#billable')).toContainText('Billable');
|
||||
await expect(page.getByRole('dialog').locator('#billableRateType')).toContainText(
|
||||
'Custom Rate'
|
||||
);
|
||||
|
||||
// Switch to Non-billable
|
||||
await page.getByRole('dialog').locator('#billable').click();
|
||||
await page.getByRole('option', { name: 'Non-billable' }).click();
|
||||
|
||||
// Rate type should still be Custom Rate (not reset)
|
||||
await expect(page.getByRole('dialog').locator('#billableRateType')).toContainText(
|
||||
'Custom Rate'
|
||||
);
|
||||
|
||||
// Submit and verify project is non-billable but keeps its custom rate
|
||||
await Promise.all([
|
||||
page.getByRole('button', { name: 'Update Project' }).click(),
|
||||
page.waitForResponse(
|
||||
async (response) =>
|
||||
response.url().includes('/projects/') &&
|
||||
response.request().method() === 'PUT' &&
|
||||
response.status() === 200 &&
|
||||
(await response.json()).data.is_billable === false &&
|
||||
(await response.json()).data.billable_rate === 20000
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('test that editing an existing billable project with default rate loads correctly', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
const newProjectName = 'Default Rate Edit Project ' + Math.floor(1 + Math.random() * 10000);
|
||||
// Create a project that is billable but has no custom rate (= default rate)
|
||||
await createProjectViaApi(ctx, {
|
||||
name: newProjectName,
|
||||
is_billable: true,
|
||||
billable_rate: null,
|
||||
});
|
||||
|
||||
await goToProjectsOverview(page);
|
||||
await expect(page.getByText(newProjectName)).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.getByRole('row').first().getByRole('button').click();
|
||||
await page.getByRole('menuitem').getByText('Edit').first().click();
|
||||
|
||||
// Verify it loaded as Billable with Default Rate
|
||||
await expect(page.getByRole('dialog').locator('#billable')).toContainText('Billable');
|
||||
await expect(page.getByRole('dialog').locator('#billableRateType')).toContainText(
|
||||
'Default Rate'
|
||||
);
|
||||
await expect(page.getByPlaceholder('Billable Rate')).toBeDisabled();
|
||||
});
|
||||
|
||||
// Sorting tests
|
||||
test('test that sorting projects by name works', async ({ page }) => {
|
||||
await goToProjectsOverview(page);
|
||||
@@ -296,8 +477,15 @@ test('test that custom billable rate is displayed correctly on project detail pa
|
||||
// Edit the project to set a custom billable rate
|
||||
await page.getByRole('row').first().getByRole('button').click();
|
||||
await page.getByRole('menuitem').getByText('Edit').first().click();
|
||||
await page.getByText('Non-Billable').click();
|
||||
await page.getByText('Custom Rate').click();
|
||||
|
||||
// Set billable default to Billable
|
||||
await page.getByRole('dialog').locator('#billable').click();
|
||||
await page.getByRole('option', { name: 'Billable', exact: true }).click();
|
||||
|
||||
// Set billable rate to Custom Rate
|
||||
await page.getByRole('dialog').locator('#billableRateType').click();
|
||||
await page.getByRole('option', { name: 'Custom Rate' }).click();
|
||||
|
||||
await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString());
|
||||
await page.getByRole('button', { name: 'Update Project' }).click();
|
||||
|
||||
|
||||
@@ -195,6 +195,69 @@ test('test that multiple tasks are displayed on project detail page', async ({ p
|
||||
await expect(page.getByText(taskName2)).toBeVisible();
|
||||
});
|
||||
|
||||
test('test that creating a new project from the task create modal project dropdown works', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
const existingProjectName = 'Existing Project ' + Math.floor(1 + Math.random() * 10000);
|
||||
const newProjectName = 'Dropdown Created Project ' + Math.floor(1 + Math.random() * 10000);
|
||||
const newTaskName = 'Task With New Project ' + Math.floor(1 + Math.random() * 10000);
|
||||
|
||||
const project = await createProjectViaApi(ctx, { name: existingProjectName });
|
||||
await page.goto(PLAYWRIGHT_BASE_URL + '/projects/' + project.id);
|
||||
|
||||
// Open the Create Task modal
|
||||
await page.getByRole('button', { name: 'Create Task' }).click();
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await page.getByPlaceholder('Task Name').fill(newTaskName);
|
||||
|
||||
// Open the project dropdown (it should show the current project)
|
||||
await page.getByRole('dialog').getByRole('button', { name: existingProjectName }).click();
|
||||
|
||||
// Click "Create new Project" at the bottom of the dropdown
|
||||
await page.getByText('Create new Project').click();
|
||||
|
||||
// The ProjectCreateModal should appear
|
||||
await expect(page.getByLabel('Project name')).toBeVisible();
|
||||
await page.getByLabel('Project name').fill(newProjectName);
|
||||
|
||||
// Submit the project creation
|
||||
await Promise.all([
|
||||
page.getByRole('button', { name: 'Create Project' }).click(),
|
||||
page.waitForResponse(
|
||||
async (response) =>
|
||||
response.url().includes('/projects') &&
|
||||
response.request().method() === 'POST' &&
|
||||
response.status() === 201 &&
|
||||
(await response.json()).data.name === newProjectName
|
||||
),
|
||||
]);
|
||||
|
||||
// The project dropdown trigger should now show the new project name
|
||||
await expect(
|
||||
page.getByRole('dialog').getByRole('button', { name: newProjectName })
|
||||
).toBeVisible();
|
||||
|
||||
// Submit the task and capture the response to get the new project ID
|
||||
const [taskResponse] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
async (response) =>
|
||||
response.url().includes('/tasks') &&
|
||||
response.request().method() === 'POST' &&
|
||||
response.status() === 201 &&
|
||||
(await response.json()).data.name === newTaskName
|
||||
),
|
||||
page.getByRole('button', { name: 'Create Task' }).click(),
|
||||
]);
|
||||
|
||||
const taskData = await taskResponse.json();
|
||||
const newProjectId = taskData.data.project_id;
|
||||
|
||||
// Navigate to the new project's page and verify the task is there
|
||||
await page.goto(PLAYWRIGHT_BASE_URL + '/projects/' + newProjectId);
|
||||
await expect(page.getByTestId('task_table')).toContainText(newTaskName);
|
||||
});
|
||||
|
||||
// =============================================
|
||||
// Employee Permission Tests
|
||||
// =============================================
|
||||
|
||||
Reference in New Issue
Block a user