Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 117 additions & 1 deletion e2e/clients.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ test('test that sorting clients by name and status works', async ({ page, ctx })

test('test that sorting clients by project count works', async ({ page, ctx }) => {
const clientWithMany = await createClientViaApi(ctx, { name: 'ManyProjects Client' });
const clientWithNone = await createClientViaApi(ctx, { name: 'NoProjects Client' });
await createClientViaApi(ctx, { name: 'NoProjects Client' });

// Create projects for the first client
await createProjectViaApi(ctx, { name: 'Proj1', client_id: clientWithMany.id });
Expand Down Expand Up @@ -374,3 +374,119 @@ test.describe('Employee Clients Restrictions', () => {
await expect(employee.page.getByText(clientName)).toBeVisible({ timeout: 10000 });
});
});

// ──────────────────────────────────────────────────
// Pagination Tests
// ──────────────────────────────────────────────────

test.describe('Clients Pagination', () => {
test.describe.configure({ timeout: 30000 });

test('test that client table paginates when there are more than 15 clients', async ({
page,
ctx,
}) => {
// Create 17 clients with zero-padded names so alphabetical sort is predictable.
// Page size is 15 → page 1 shows indices 00–14, page 2 shows 15–16.
const seed = Math.floor(Math.random() * 100000);
const prefix = `PaginationClient ${seed} `;
await Promise.all(
Array.from({ length: 17 }, (_, i) =>
createClientViaApi(ctx, { name: prefix + String(i).padStart(2, '0') })
)
);

await goToClientsOverview(page);
await clearClientTableState(page);
await page.reload();

// Default sort is name asc; first 15 clients (00–14) on page 1.
await expect(page.getByText(prefix + '00')).toBeVisible({ timeout: 10000 });
await expect(page.getByRole('button', { name: 'Next Page' })).toBeVisible();
// Client 15 should be on page 2, not visible on page 1.
await expect(page.getByText(prefix + '15')).not.toBeVisible();

// Exactly 15 data rows mounted on page 1.
await expect(page.getByRole('row')).toHaveCount(15);

// Navigation to page 2.
await page.getByRole('button', { name: 'Next Page' }).click();
await expect(page.getByText(prefix + '15')).toBeVisible();
await expect(page.getByText(prefix + '00')).not.toBeVisible();
// Page 2 contains the remaining 2 clients.
await expect(page.getByRole('row')).toHaveCount(2);

// Back to page 1 via Previous Page.
await page.getByRole('button', { name: 'Previous Page' }).click();
await expect(page.getByText(prefix + '00')).toBeVisible();
await expect(page.getByText(prefix + '15')).not.toBeVisible();

// First / Last page jumps.
await page.getByRole('button', { name: 'Last Page' }).click();
await expect(page.getByText(prefix + '15')).toBeVisible();
await page.getByRole('button', { name: 'First Page' }).click();
await expect(page.getByText(prefix + '00')).toBeVisible();
await expect(page.getByText(prefix + '15')).not.toBeVisible();

// Direct page-number button navigation + selected state.
await page.getByRole('button', { name: 'Page 2' }).click();
await expect(page.getByText(prefix + '15')).toBeVisible();
await expect(page.getByRole('button', { name: 'Page 2' })).toHaveAttribute(
'aria-current',
'page'
);
});

test('test that client pagination is not shown when there are 15 or fewer clients', async ({
page,
ctx,
}) => {
await Promise.all(
Array.from({ length: 10 }, (_, i) =>
createClientViaApi(ctx, {
name: `FewClient ${Math.floor(Math.random() * 100000)} ${i}`,
})
)
);

await goToClientsOverview(page);
await clearClientTableState(page);
await page.reload();

await expect(page.getByTestId('client_table')).toBeVisible();
await expect(page.getByRole('button', { name: 'Next Page' })).toHaveCount(0);
});

test('test that changing the sort resets client pagination to page 1', async ({
page,
ctx,
}) => {
const seed = Math.floor(Math.random() * 100000);
const prefix = `SortPagClient ${seed} `;
await Promise.all(
Array.from({ length: 17 }, (_, i) =>
createClientViaApi(ctx, { name: prefix + String(i).padStart(2, '0') })
)
);

await goToClientsOverview(page);
await clearClientTableState(page);
await page.reload();

await expect(page.getByText(prefix + '00')).toBeVisible({ timeout: 10000 });

// Go to page 2.
await page.getByRole('button', { name: 'Next Page' }).click();
await expect(page.getByText(prefix + '15')).toBeVisible();

// Sort by name descending.
const table = page.getByTestId('client_table');
const nameHeader = table.getByText('Name').first();
await nameHeader.click();

// Pagination reset to page 1; desc order → 16, 15 visible, 00 on page 2.
await expect(page.getByText(prefix + '16')).toBeVisible();
await expect(page.getByText(prefix + '15')).toBeVisible();
await expect(page.getByText(prefix + '00')).not.toBeVisible();
});
});
153 changes: 153 additions & 0 deletions e2e/projects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,43 @@ test('test that archiving and unarchiving projects works', async ({ page, ctx })
await expect(page.getByText(newProjectName)).toBeVisible();
});

test('test that the client can be changed in the edit project modal', async ({ page, ctx }) => {
const projectName = 'Edit Client Project ' + Math.floor(1 + Math.random() * 100000);
const clientName = 'Assigned Client ' + Math.floor(1 + Math.random() * 100000);
await createProjectViaApi(ctx, { name: projectName });
const client = await createClientViaApi(ctx, { name: clientName });

await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
await expect(page.getByText(projectName)).toBeVisible({ timeout: 10000 });

// Open the project's Edit modal.
await page.getByRole('row').first().getByRole('button').click();
await page.getByRole('menuitem').getByText('Edit').first().click();
await expect(page.getByRole('dialog')).toBeVisible();

// Open the client dropdown (currently "No Client"), confirm it focuses, and pick the client.
await page.getByRole('dialog').getByRole('button', { name: 'No Client' }).click();
const clientSearch = page.getByPlaceholder('Search for a client...');
await expect(clientSearch).toBeFocused();
await clientSearch.fill(clientName);
await page.getByRole('option', { name: clientName }).click();

// The trigger updates to the chosen client.
await expect(page.getByRole('dialog').getByRole('button', { name: clientName })).toBeVisible();

// Saving persists the client assignment.
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.client_id === client.id
),
]);
});

test('test that updating billable rate works with existing time entries', async ({ page, ctx }) => {
const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
const newBillableRate = Math.round(Math.random() * 10000);
Expand Down Expand Up @@ -1054,3 +1091,119 @@ test.describe('Employee Billable Rate Visibility', () => {
await expect(projectRow).toContainText('200');
});
});

// ──────────────────────────────────────────────────
// Pagination Tests
// ──────────────────────────────────────────────────

test.describe('Projects Pagination', () => {
test.describe.configure({ timeout: 30000 });

test('test that project table paginates when there are more than 15 projects', async ({
page,
ctx,
}) => {
// Create 17 projects with zero-padded names so alphabetical sort is predictable.
// Page size is 15 → page 1 shows indices 00–14, page 2 shows 15–16.
const seed = Math.floor(Math.random() * 100000);
const prefix = `PaginationProj ${seed} `;
await Promise.all(
Array.from({ length: 17 }, (_, i) =>
createProjectViaApi(ctx, { name: prefix + String(i).padStart(2, '0') })
)
);

await goToProjectsOverview(page);
await clearProjectTableState(page);
await page.reload();

// Default sort is name asc; first 15 projects (00–14) should be on page 1.
await expect(page.getByText(prefix + '00')).toBeVisible({ timeout: 10000 });
await expect(page.getByRole('button', { name: 'Next Page' })).toBeVisible();
// Project 15 should be on page 2, not visible on page 1.
await expect(page.getByText(prefix + '15')).not.toBeVisible();

// Exactly 15 data rows should be mounted on page 1.
await expect(page.getByRole('row')).toHaveCount(15);

// Go to page 2.
await page.getByRole('button', { name: 'Next Page' }).click();
await expect(page.getByText(prefix + '15')).toBeVisible();
await expect(page.getByText(prefix + '00')).not.toBeVisible();
// Page 2 contains the remaining 2 projects (15, 16).
await expect(page.getByRole('row')).toHaveCount(2);

// Return to page 1 via Previous Page.
await page.getByRole('button', { name: 'Previous Page' }).click();
await expect(page.getByText(prefix + '00')).toBeVisible();
await expect(page.getByText(prefix + '15')).not.toBeVisible();

// Jump to last page then back to first page.
await page.getByRole('button', { name: 'Last Page' }).click();
await expect(page.getByText(prefix + '15')).toBeVisible();
await page.getByRole('button', { name: 'First Page' }).click();
await expect(page.getByText(prefix + '00')).toBeVisible();
await expect(page.getByText(prefix + '15')).not.toBeVisible();

// Direct page-number button navigation.
await page.getByRole('button', { name: 'Page 2' }).click();
await expect(page.getByText(prefix + '15')).toBeVisible();
// Page 2 button should be marked as selected.
await expect(page.getByRole('button', { name: 'Page 2' })).toHaveAttribute(
'aria-current',
'page'
);
});

test('test that project pagination is not shown when there are 15 or fewer projects', async ({
page,
ctx,
}) => {
await Promise.all(
Array.from({ length: 10 }, (_, i) =>
createProjectViaApi(ctx, {
name: `FewProj ${Math.floor(Math.random() * 100000)} ${i}`,
})
)
);

await goToProjectsOverview(page);
await clearProjectTableState(page);
await page.reload();

await expect(page.getByTestId('project_table')).toBeVisible();
await expect(page.getByRole('button', { name: 'Next Page' })).toHaveCount(0);
});

test('test that changing the sort resets pagination to page 1', async ({ page, ctx }) => {
const seed = Math.floor(Math.random() * 100000);
const prefix = `SortPagProj ${seed} `;
await Promise.all(
Array.from({ length: 17 }, (_, i) =>
createProjectViaApi(ctx, { name: prefix + String(i).padStart(2, '0') })
)
);

await goToProjectsOverview(page);
await clearProjectTableState(page);
await page.reload();

await expect(page.getByText(prefix + '00')).toBeVisible({ timeout: 10000 });

// Go to page 2.
await page.getByRole('button', { name: 'Next Page' }).click();
await expect(page.getByText(prefix + '15')).toBeVisible();

// Sort by name descending: header click toggles asc → desc.
const nameHeader = page
.locator('[data-testid="project_table"] .select-none', { hasText: 'Name' })
.first();
await nameHeader.click();

// After sorting, pagination resets to page 1; desc order → 16, 15, ... 02 visible.
await expect(page.getByText(prefix + '16')).toBeVisible();
await expect(page.getByText(prefix + '15')).toBeVisible();
// Index 00 should now be on page 2 (last in desc order).
await expect(page.getByText(prefix + '00')).not.toBeVisible();
});
});
Loading
Loading