Files
solidtime/e2e/project-members.spec.ts
2026-02-11 17:29:41 +01:00

253 lines
9.4 KiB
TypeScript

import { expect } from '@playwright/test';
import type { Page } from '@playwright/test';
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
import { test } from '../playwright/fixtures';
import { formatCentsWithOrganizationDefaults } from './utils/money';
import { createProjectViaApi, createProjectMemberViaApi, type TestContext } from './utils/api';
async function createProjectWithMemberViaApi(ctx: TestContext, page: Page, projectName: string) {
const project = await createProjectViaApi(ctx, { name: projectName });
await createProjectMemberViaApi(ctx, project.id, { member_id: ctx.memberId });
// Navigate to the project detail page
await page.goto(PLAYWRIGHT_BASE_URL + '/projects/' + project.id);
await expect(page.getByTestId('project_member_table').getByRole('row').first()).toBeVisible();
return project;
}
test('test that updating project member billable rate works for existing time entries', async ({
page,
ctx,
}) => {
const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
const newBillableRate = Math.round(Math.random() * 10000);
await createProjectWithMemberViaApi(ctx, page, newProjectName);
await page
.getByTestId('project_member_table')
.getByRole('row')
.first()
.getByRole('button')
.click();
await page.getByRole('menuitem', { name: 'Edit Project Member' }).click();
await page.getByLabel('Billable Rate').fill(newBillableRate.toString());
await page.getByRole('button', { name: 'Update Project Member' }).click();
await Promise.all([
page.getByRole('button', { name: 'Yes, update existing time' }).click(),
page.waitForRequest(
async (request) =>
request.url().includes('/project-members/') &&
request.method() === 'PUT' &&
request.postDataJSON().billable_rate === newBillableRate * 100
),
page.waitForResponse(
async (response) =>
response.url().includes('/project-members/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.billable_rate === newBillableRate * 100
),
]);
await expect(
page
.getByRole('row')
.first()
.getByText(formatCentsWithOrganizationDefaults(newBillableRate * 100))
).toBeVisible();
});
test('test that project member edit modal can be cancelled without sending API request', async ({
page,
ctx,
}) => {
const projectName = 'Cancel Test ' + Math.floor(1 + Math.random() * 10000);
await createProjectWithMemberViaApi(ctx, page, projectName);
// Open the edit modal
await page
.getByTestId('project_member_table')
.getByRole('row')
.first()
.getByRole('button')
.click();
await page.getByRole('menuitem', { name: 'Edit Project Member' }).click();
// Verify the modal is open and shows the member name
await expect(page.getByRole('heading', { name: 'Edit Project Member' })).toBeVisible();
await expect(page.getByRole('dialog').getByText('John Doe')).toBeVisible();
// Enter a new billable rate
await page.getByLabel('Billable Rate').fill('999');
// Set up listener to verify no PUT request is sent
let putRequestSent = false;
page.on('request', (request) => {
if (request.url().includes('/project-members/') && request.method() === 'PUT') {
putRequestSent = true;
}
});
// Click Cancel
await page.getByRole('button', { name: 'Cancel' }).click();
// Verify the modal is closed
await expect(page.getByRole('heading', { name: 'Edit Project Member' })).not.toBeVisible();
// Verify no PUT request was sent
expect(putRequestSent).toBe(false);
});
test('test that project member update without billable rate change skips confirmation and completes', async ({
page,
ctx,
}) => {
const projectName = 'No Change ' + Math.floor(1 + Math.random() * 10000);
await createProjectWithMemberViaApi(ctx, page, projectName);
// Open the edit modal
await page
.getByTestId('project_member_table')
.getByRole('row')
.first()
.getByRole('button')
.click();
await page.getByRole('menuitem', { name: 'Edit Project Member' }).click();
// Click Update without changing anything - no confirmation modal since rate didn't change
await Promise.all([
page.waitForResponse(
(response) =>
response.url().includes('/project-members/') &&
response.request().method() === 'PUT' &&
response.status() === 200
),
page.getByRole('button', { name: 'Update Project Member' }).click(),
]);
// Verify the edit modal is closed (confirmation modal was skipped)
await expect(page.getByRole('heading', { name: 'Edit Project Member' })).not.toBeVisible();
});
test('test that billable rate confirmation modal can be cancelled without sending API request', async ({
page,
ctx,
}) => {
const projectName = 'Rate Cancel ' + Math.floor(1 + Math.random() * 10000);
const newBillableRate = Math.round(Math.random() * 10000);
await createProjectWithMemberViaApi(ctx, page, projectName);
// Open the edit modal
await page
.getByTestId('project_member_table')
.getByRole('row')
.first()
.getByRole('button')
.click();
await page.getByRole('menuitem', { name: 'Edit Project Member' }).click();
// Change the billable rate
await page.getByLabel('Billable Rate').fill(newBillableRate.toString());
// Set up listener to verify no PUT request is sent
let putRequestSent = false;
page.on('request', (request) => {
if (request.url().includes('/project-members/') && request.method() === 'PUT') {
putRequestSent = true;
}
});
// Click Update - this should show the confirmation modal
await page.getByRole('button', { name: 'Update Project Member' }).click();
// Verify the confirmation modal is shown
await expect(page.getByText('update all existing time entries')).toBeVisible();
// Click Cancel to close the confirmation modal without updating
await page.getByRole('button', { name: 'Cancel' }).click();
// Verify the confirmation modal is closed but edit modal is still open
await expect(page.getByText('update all existing time entries')).not.toBeVisible();
await expect(page.getByRole('heading', { name: 'Edit Project Member' })).toBeVisible();
// Close the edit modal
await page.getByRole('dialog').getByRole('button', { name: 'Cancel' }).click();
// Verify the edit modal is closed
await expect(page.getByRole('heading', { name: 'Edit Project Member' })).not.toBeVisible();
// Verify no PUT request was sent
expect(putRequestSent).toBe(false);
});
test('test that clearing billable rate reverts to project default', async ({ page, ctx }) => {
const projectName = 'Revert Default ' + Math.floor(1 + Math.random() * 10000);
const customRate = Math.round(100 + Math.random() * 10000);
await createProjectWithMemberViaApi(ctx, page, projectName);
// Verify the billable rate shows "--" (project default) initially
await expect(
page.getByTestId('project_member_table').getByRole('row').first().getByText('--')
).toBeVisible();
// Set a custom billable rate
await page
.getByTestId('project_member_table')
.getByRole('row')
.first()
.getByRole('button')
.click();
await page.getByRole('menuitem', { name: 'Edit Project Member' }).click();
await page.getByLabel('Billable Rate').fill(customRate.toString());
await page.getByRole('button', { name: 'Update Project Member' }).click();
// Confirm the billable rate update
await Promise.all([
page.getByRole('button', { name: 'Yes, update existing time' }).click(),
page.waitForResponse(
(response) =>
response.url().includes('/project-members/') &&
response.request().method() === 'PUT' &&
response.status() === 200
),
]);
// Verify the custom rate is shown in the table (not "--")
await expect(
page.getByTestId('project_member_table').getByRole('row').first().getByText('--')
).not.toBeVisible();
// Now clear the billable rate to revert to project default
await page
.getByTestId('project_member_table')
.getByRole('row')
.first()
.getByRole('button')
.click();
await page.getByRole('menuitem', { name: 'Edit Project Member' }).click();
// Set billable rate to 0 to revert to project default
await page.getByLabel('Billable Rate').fill('0');
await page.getByRole('button', { name: 'Update Project Member' }).click();
// Confirm the billable rate update
await Promise.all([
page.getByRole('button', { name: 'Yes, update existing time' }).click(),
page.waitForResponse(
(response) =>
response.url().includes('/project-members/') &&
response.request().method() === 'PUT' &&
response.status() === 200
),
]);
// Verify the billable rate shows "--" again (project default)
await expect(
page.getByTestId('project_member_table').getByRole('row').first().getByText('--')
).toBeVisible();
});