mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
add pagination to client and project table
This commit is contained in:
@@ -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 });
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1054,3 +1054,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -717,3 +717,108 @@ test('test that keyboard navigation works in multiselect dropdown', async ({ pag
|
||||
page.getByRole('button', { name: 'Projects' }).first().getByText('1')
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Pagination Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test.describe('Reporting Detailed Pagination', () => {
|
||||
test('test that detailed reporting paginates when there are more than 15 time entries', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
// The detailed report paginates server-side with a page limit of 15.
|
||||
// Create 17 time entries on a single project so we get exactly 2 pages.
|
||||
const seed = Math.floor(Math.random() * 100000);
|
||||
const projectName = `ReportPagProj ${seed}`;
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
const descriptions = Array.from(
|
||||
{ length: 17 },
|
||||
(_, i) => `ReportPagEntry ${String(i).padStart(2, '0')} ${seed}`
|
||||
);
|
||||
await Promise.all(
|
||||
descriptions.map((description) =>
|
||||
createTimeEntryViaApi(ctx, {
|
||||
description,
|
||||
duration: '30min',
|
||||
projectId: project.id,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
await expect(page.getByText(descriptions[0]!).first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Pagination nav should be rendered.
|
||||
await expect(page.getByRole('button', { name: 'Next Page' })).toBeVisible();
|
||||
|
||||
// Collect which descriptions are currently visible on page 1.
|
||||
const visiblePage1 = new Set<string>();
|
||||
for (const description of descriptions) {
|
||||
if ((await page.getByText(description).count()) > 0) {
|
||||
visiblePage1.add(description);
|
||||
}
|
||||
}
|
||||
// The page limit is 15 → exactly 15 entries visible on page 1.
|
||||
expect(visiblePage1.size).toBe(15);
|
||||
|
||||
// Go to page 2 and wait for the server fetch.
|
||||
await Promise.all([
|
||||
page.getByRole('button', { name: 'Next Page' }).click(),
|
||||
waitForDetailedReportingUpdate(page),
|
||||
]);
|
||||
|
||||
const visiblePage2 = new Set<string>();
|
||||
for (const description of descriptions) {
|
||||
if ((await page.getByText(description).count()) > 0) {
|
||||
visiblePage2.add(description);
|
||||
}
|
||||
}
|
||||
// Page 2 should hold the remaining 2 entries, disjoint from page 1.
|
||||
expect(visiblePage2.size).toBe(2);
|
||||
for (const description of visiblePage2) {
|
||||
expect(visiblePage1.has(description)).toBe(false);
|
||||
}
|
||||
// Across both pages, all 17 entries should have been visible.
|
||||
expect(visiblePage1.size + visiblePage2.size).toBe(17);
|
||||
|
||||
// Page 2 button is selected.
|
||||
await expect(page.getByRole('button', { name: 'Page 2' })).toHaveAttribute(
|
||||
'aria-current',
|
||||
'page'
|
||||
);
|
||||
|
||||
// Previous page returns to page 1.
|
||||
await Promise.all([
|
||||
page.getByRole('button', { name: 'Previous Page' }).click(),
|
||||
waitForDetailedReportingUpdate(page),
|
||||
]);
|
||||
expect((await page.getByText(descriptions[0]!).count()) > 0).toBe(true);
|
||||
});
|
||||
|
||||
test('test that reporting pagination is not shown when there are 15 or fewer time entries', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
const seed = Math.floor(Math.random() * 100000);
|
||||
const projectName = `FewEntriesProj ${seed}`;
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
await Promise.all(
|
||||
Array.from({ length: 5 }, (_, i) =>
|
||||
createTimeEntryViaApi(ctx, {
|
||||
description: `FewEntries ${i} ${seed}`,
|
||||
duration: '30min',
|
||||
projectId: project.id,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
await expect(page.getByText(`FewEntries 0 ${seed}`).first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByRole('button', { name: 'Next Page' })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user