mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 20:52:14 +01:00
Expand e2e test coverage migrate to API-based data setup
This commit is contained in:
@@ -1,47 +1,52 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '../playwright/fixtures';
|
||||
import { goToReportingDetailed, waitForDetailedReportingUpdate } from './utils/reporting';
|
||||
import {
|
||||
goToReportingDetailed,
|
||||
createProject,
|
||||
createClient,
|
||||
createProjectWithClient,
|
||||
createTask,
|
||||
createTimeEntryWithProject,
|
||||
createTimeEntryWithProjectAndTask,
|
||||
createTimeEntryWithTag,
|
||||
createBareTimeEntry,
|
||||
waitForDetailedReportingUpdate,
|
||||
} from './utils/reporting';
|
||||
createProjectViaApi,
|
||||
createClientViaApi,
|
||||
createTaskViaApi,
|
||||
createTimeEntryViaApi,
|
||||
createTimeEntryWithTagViaApi,
|
||||
createBareTimeEntryViaApi,
|
||||
} from './utils/api';
|
||||
|
||||
// Each test registers a new user and creates test data, which needs more time
|
||||
test.describe.configure({ timeout: 60000 });
|
||||
// Each test registers a new user and creates test data via API
|
||||
test.describe.configure({ timeout: 30000 });
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Basic Detailed View Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that detailed view shows time entries correctly', async ({ page }) => {
|
||||
test('test that detailed view shows time entries correctly', async ({ page, ctx }) => {
|
||||
const projectName = 'Detailed View Project ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, projectName);
|
||||
await createTimeEntryWithProject(page, projectName, '1h');
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName}`,
|
||||
duration: '1h',
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
// 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('Entry for ' + projectName, { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(projectName, { exact: true }).first()).toBeVisible();
|
||||
await expect(page.locator('input[name="Duration"]').first()).toHaveValue('1h 00min');
|
||||
await expect(page.getByText('Entry for ' + projectName, { exact: true }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('test that updating duration in detailed view works correctly', async ({ page }) => {
|
||||
test('test that updating duration in detailed view works correctly', async ({ page, ctx }) => {
|
||||
const projectName = 'Duration Update Project ' + Math.floor(Math.random() * 10000);
|
||||
const initialDuration = '1h';
|
||||
const updatedDuration = '2h 30min';
|
||||
|
||||
await createProject(page, projectName);
|
||||
await createTimeEntryWithProject(page, projectName, initialDuration);
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName}`,
|
||||
duration: initialDuration,
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
// Go to detailed reporting view
|
||||
await goToReportingDetailed(page);
|
||||
@@ -65,20 +70,31 @@ test('test that updating duration in detailed view works correctly', async ({ pa
|
||||
// Project Filter Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that project multiselect filters work on detailed reporting page', async ({ page }) => {
|
||||
test('test that project multiselect filters work on detailed reporting page', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
const project1 = 'DetailProj1 ' + Math.floor(Math.random() * 10000);
|
||||
const project2 = 'DetailProj2 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, project1);
|
||||
await createProject(page, project2);
|
||||
await createTimeEntryWithProject(page, project1, '1h');
|
||||
await createTimeEntryWithProject(page, project2, '2h');
|
||||
const p1 = await createProjectViaApi(ctx, { name: project1 });
|
||||
const p2 = await createProjectViaApi(ctx, { name: project2 });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project1}`,
|
||||
duration: '1h',
|
||||
projectId: p1.id,
|
||||
});
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project2}`,
|
||||
duration: '2h',
|
||||
projectId: p2.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
// Wait for initial data load
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`).first()).toBeVisible();
|
||||
|
||||
// Open project multiselect and select project1
|
||||
await page.getByRole('button', { name: 'Projects' }).first().click();
|
||||
@@ -87,29 +103,40 @@ test('test that project multiselect filters work on detailed reporting page', as
|
||||
await Promise.all([page.keyboard.press('Escape'), waitForDetailedReportingUpdate(page)]);
|
||||
|
||||
// Verify only project1 entry is shown
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`)).not.toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Client Filter Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that client multiselect filters work on detailed reporting page', async ({ page }) => {
|
||||
test('test that client multiselect filters work on detailed reporting page', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
const client1 = 'DetailClient1 ' + Math.floor(Math.random() * 10000);
|
||||
const project1 = 'DetailClientProj1 ' + Math.floor(Math.random() * 10000);
|
||||
const project2 = 'DetailClientProj2 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createClient(page, client1);
|
||||
await createProjectWithClient(page, project1, client1);
|
||||
await createProject(page, project2);
|
||||
await createTimeEntryWithProject(page, project1, '1h');
|
||||
await createTimeEntryWithProject(page, project2, '2h');
|
||||
const c1 = await createClientViaApi(ctx, { name: client1 });
|
||||
const p1 = await createProjectViaApi(ctx, { name: project1, client_id: c1.id });
|
||||
const p2 = await createProjectViaApi(ctx, { name: project2 });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project1}`,
|
||||
duration: '1h',
|
||||
projectId: p1.id,
|
||||
});
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project2}`,
|
||||
duration: '2h',
|
||||
projectId: p2.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`).first()).toBeVisible();
|
||||
|
||||
// Filter by client1
|
||||
await page.getByRole('button', { name: 'Clients' }).first().click();
|
||||
@@ -118,30 +145,40 @@ test('test that client multiselect filters work on detailed reporting page', asy
|
||||
await Promise.all([page.keyboard.press('Escape'), waitForDetailedReportingUpdate(page)]);
|
||||
|
||||
// Only entries for project1 (with client1) should be visible
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`)).not.toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Task Filter Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that task multiselect dropdown filters reporting by task', async ({ page }) => {
|
||||
test('test that task multiselect dropdown filters reporting by task', async ({ page, ctx }) => {
|
||||
const projectName = 'TaskFilterProj ' + Math.floor(Math.random() * 10000);
|
||||
const task1 = 'TaskFilter1 ' + Math.floor(Math.random() * 10000);
|
||||
const task2 = 'TaskFilter2 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, projectName);
|
||||
await createTask(page, projectName, task1);
|
||||
await createTask(page, projectName, task2);
|
||||
await createTimeEntryWithProjectAndTask(page, projectName, task1, '1h');
|
||||
await createTimeEntryWithProjectAndTask(page, projectName, task2, '2h');
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
const t1 = await createTaskViaApi(ctx, { name: task1, project_id: project.id });
|
||||
const t2 = await createTaskViaApi(ctx, { name: task2, project_id: project.id });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName} - ${task1}`,
|
||||
duration: '1h',
|
||||
projectId: project.id,
|
||||
taskId: t1.id,
|
||||
});
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName} - ${task2}`,
|
||||
duration: '2h',
|
||||
projectId: project.id,
|
||||
taskId: t2.id,
|
||||
});
|
||||
|
||||
// Use the detailed view to verify task filtering (shows individual entries)
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task2}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task2}`).first()).toBeVisible();
|
||||
|
||||
// Open task multiselect dropdown
|
||||
await page.getByRole('button', { name: 'Tasks' }).first().click();
|
||||
@@ -159,26 +196,36 @@ test('test that task multiselect dropdown filters reporting by task', async ({ p
|
||||
await expect(page.getByRole('button', { name: 'Tasks' }).first().getByText('1')).toBeVisible();
|
||||
|
||||
// Verify only task1 entry is shown
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task2}`)).not.toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task2}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('test that selecting multiple tasks shows correct badge count', async ({ page }) => {
|
||||
test('test that selecting multiple tasks shows correct badge count', async ({ page, ctx }) => {
|
||||
const projectName = 'MultiTaskProj ' + Math.floor(Math.random() * 10000);
|
||||
const task1 = 'MultiTask1 ' + Math.floor(Math.random() * 10000);
|
||||
const task2 = 'MultiTask2 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, projectName);
|
||||
await createTask(page, projectName, task1);
|
||||
await createTask(page, projectName, task2);
|
||||
await createTimeEntryWithProjectAndTask(page, projectName, task1, '1h');
|
||||
await createTimeEntryWithProjectAndTask(page, projectName, task2, '2h');
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
const t1 = await createTaskViaApi(ctx, { name: task1, project_id: project.id });
|
||||
const t2 = await createTaskViaApi(ctx, { name: task2, project_id: project.id });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName} - ${task1}`,
|
||||
duration: '1h',
|
||||
projectId: project.id,
|
||||
taskId: t1.id,
|
||||
});
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName} - ${task2}`,
|
||||
duration: '2h',
|
||||
projectId: project.id,
|
||||
taskId: t2.id,
|
||||
});
|
||||
|
||||
// Use the detailed view to verify task filtering
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task2}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task2}`).first()).toBeVisible();
|
||||
|
||||
// Select both tasks
|
||||
await page.getByRole('button', { name: 'Tasks' }).first().click();
|
||||
@@ -191,20 +238,25 @@ test('test that selecting multiple tasks shows correct badge count', async ({ pa
|
||||
await expect(page.getByRole('button', { name: 'Tasks' }).first().getByText('2')).toBeVisible();
|
||||
|
||||
// Verify both task entries are shown
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task2}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task2}`).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('test that deselecting a task removes the filter', async ({ page }) => {
|
||||
test('test that deselecting a task removes the filter', async ({ page, ctx }) => {
|
||||
const projectName = 'TaskDeselectProj ' + Math.floor(Math.random() * 10000);
|
||||
const task1 = 'TaskDeselect1 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, projectName);
|
||||
await createTask(page, projectName, task1);
|
||||
await createTimeEntryWithProjectAndTask(page, projectName, task1, '1h');
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
const t1 = await createTaskViaApi(ctx, { name: task1, project_id: project.id });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName} - ${task1}`,
|
||||
duration: '1h',
|
||||
projectId: project.id,
|
||||
taskId: t1.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`).first()).toBeVisible();
|
||||
|
||||
// Select task
|
||||
await page.getByRole('button', { name: 'Tasks' }).first().click();
|
||||
@@ -227,14 +279,21 @@ test('test that deselecting a task removes the filter', async ({ page }) => {
|
||||
// Member Filter Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that member multiselect filters work on detailed reporting page', async ({ page }) => {
|
||||
test('test that member multiselect filters work on detailed reporting page', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
const projectName = 'DetailMemberProj ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, projectName);
|
||||
await createTimeEntryWithProject(page, projectName, '1h');
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName}`,
|
||||
duration: '1h',
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
await expect(page.getByText(`Entry for ${projectName}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName}`).first()).toBeVisible();
|
||||
|
||||
// Filter by the current member
|
||||
await page.getByRole('button', { name: 'Members' }).first().click();
|
||||
@@ -243,7 +302,7 @@ test('test that member multiselect filters work on detailed reporting page', asy
|
||||
await Promise.all([page.keyboard.press('Escape'), waitForDetailedReportingUpdate(page)]);
|
||||
|
||||
// Data should still be visible since all entries belong to this member
|
||||
await expect(page.getByText(`Entry for ${projectName}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName}`).first()).toBeVisible();
|
||||
|
||||
// Verify badge shows count of 1
|
||||
await expect(
|
||||
@@ -255,17 +314,17 @@ test('test that member multiselect filters work on detailed reporting page', asy
|
||||
// Tag Filter Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that tag filter works on detailed reporting page', async ({ page }) => {
|
||||
test('test that tag filter works on detailed reporting page', async ({ page, ctx }) => {
|
||||
const tag1 = 'DetailTag1 ' + Math.floor(Math.random() * 10000);
|
||||
const tag2 = 'DetailTag2 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createTimeEntryWithTag(page, tag1, '1h');
|
||||
await createTimeEntryWithTag(page, tag2, '2h');
|
||||
await createTimeEntryWithTagViaApi(ctx, tag1, '1h');
|
||||
await createTimeEntryWithTagViaApi(ctx, tag2, '2h');
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag2}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag2}`).first()).toBeVisible();
|
||||
|
||||
// Filter by tag1
|
||||
await page.getByRole('button', { name: 'Tags' }).click();
|
||||
@@ -273,22 +332,26 @@ test('test that tag filter works on detailed reporting page', async ({ page }) =
|
||||
|
||||
await Promise.all([page.keyboard.press('Escape'), waitForDetailedReportingUpdate(page)]);
|
||||
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag2}`)).not.toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag2}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Billable Filter Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that billable filter works on detailed reporting page', async ({ page }) => {
|
||||
test('test that billable filter works on detailed reporting page', async ({ page, ctx }) => {
|
||||
const projectName = 'DetailBillProj ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, projectName);
|
||||
await createTimeEntryWithProject(page, projectName, '1h');
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName}`,
|
||||
duration: '1h',
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
await expect(page.getByText(`Entry for ${projectName}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName}`).first()).toBeVisible();
|
||||
|
||||
// Filter by billable only
|
||||
await page.getByRole('combobox').filter({ hasText: 'Billable' }).click();
|
||||
@@ -316,21 +379,30 @@ test('test that billable filter works on detailed reporting page', async ({ page
|
||||
// Combined Filter Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that combining project and task filters narrows results', async ({ page }) => {
|
||||
test('test that combining project and task filters narrows results', async ({ page, ctx }) => {
|
||||
const projectName = 'CombinedProj ' + Math.floor(Math.random() * 10000);
|
||||
const otherProject = 'OtherCombProj ' + Math.floor(Math.random() * 10000);
|
||||
const task1 = 'CombinedTask1 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, projectName);
|
||||
await createProject(page, otherProject);
|
||||
await createTask(page, projectName, task1);
|
||||
await createTimeEntryWithProjectAndTask(page, projectName, task1, '1h');
|
||||
await createTimeEntryWithProject(page, otherProject, '2h');
|
||||
const p1 = await createProjectViaApi(ctx, { name: projectName });
|
||||
const p2 = await createProjectViaApi(ctx, { name: otherProject });
|
||||
const t1 = await createTaskViaApi(ctx, { name: task1, project_id: p1.id });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName} - ${task1}`,
|
||||
duration: '1h',
|
||||
projectId: p1.id,
|
||||
taskId: t1.id,
|
||||
});
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${otherProject}`,
|
||||
duration: '2h',
|
||||
projectId: p2.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${otherProject}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${otherProject}`).first()).toBeVisible();
|
||||
|
||||
// Filter by project
|
||||
await page.getByRole('button', { name: 'Projects' }).first().click();
|
||||
@@ -349,27 +421,36 @@ test('test that combining project and task filters narrows results', async ({ pa
|
||||
await expect(page.getByRole('button', { name: 'Tasks' }).first().getByText('1')).toBeVisible();
|
||||
|
||||
// Verify only the combined entry is shown
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${otherProject}`)).not.toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${otherProject}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('test that combining client and member filters narrows results on detailed page', async ({
|
||||
page,
|
||||
ctx,
|
||||
}) => {
|
||||
const client1 = 'CombClient ' + Math.floor(Math.random() * 10000);
|
||||
const project1 = 'CombClientProj ' + Math.floor(Math.random() * 10000);
|
||||
const project2 = 'CombNoClientProj ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createClient(page, client1);
|
||||
await createProjectWithClient(page, project1, client1);
|
||||
await createProject(page, project2);
|
||||
await createTimeEntryWithProject(page, project1, '1h');
|
||||
await createTimeEntryWithProject(page, project2, '2h');
|
||||
const c1 = await createClientViaApi(ctx, { name: client1 });
|
||||
const p1 = await createProjectViaApi(ctx, { name: project1, client_id: c1.id });
|
||||
const p2 = await createProjectViaApi(ctx, { name: project2 });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project1}`,
|
||||
duration: '1h',
|
||||
projectId: p1.id,
|
||||
});
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project2}`,
|
||||
duration: '2h',
|
||||
projectId: p2.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`).first()).toBeVisible();
|
||||
|
||||
// Filter by client
|
||||
await page.getByRole('button', { name: 'Clients' }).first().click();
|
||||
@@ -382,8 +463,8 @@ test('test that combining client and member filters narrows results on detailed
|
||||
await Promise.all([page.keyboard.press('Escape'), waitForDetailedReportingUpdate(page)]);
|
||||
|
||||
// Only project1 entry should be visible (filtered by client + member)
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`)).not.toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project2}`).first()).not.toBeVisible();
|
||||
|
||||
// Both badges should show count of 1
|
||||
await expect(
|
||||
@@ -394,22 +475,26 @@ test('test that combining client and member filters narrows results on detailed
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('test that combining tag and project filters narrows results', async ({ page }) => {
|
||||
test('test that combining tag and project filters narrows results', async ({ page, ctx }) => {
|
||||
const tag1 = 'CombTag ' + Math.floor(Math.random() * 10000);
|
||||
const project1 = 'CombTagProj ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, project1);
|
||||
const p1 = await createProjectViaApi(ctx, { name: project1 });
|
||||
|
||||
// Create a time entry with a project (no tag)
|
||||
await createTimeEntryWithProject(page, project1, '1h');
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project1}`,
|
||||
duration: '1h',
|
||||
projectId: p1.id,
|
||||
});
|
||||
|
||||
// Create a time entry with a tag (no specific project)
|
||||
await createTimeEntryWithTag(page, tag1, '2h');
|
||||
await createTimeEntryWithTagViaApi(ctx, tag1, '2h');
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`).first()).toBeVisible();
|
||||
|
||||
// Filter by project
|
||||
await page.getByRole('button', { name: 'Projects' }).first().click();
|
||||
@@ -417,25 +502,29 @@ test('test that combining tag and project filters narrows results', async ({ pag
|
||||
await Promise.all([page.keyboard.press('Escape'), waitForDetailedReportingUpdate(page)]);
|
||||
|
||||
// Only the project entry should be visible (tagged entry has no project)
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`)).not.toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// "No X" Filter Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that "No Project" filter shows entries without a project', async ({ page }) => {
|
||||
test('test that "No Project" filter shows entries without a project', async ({ page, ctx }) => {
|
||||
const project1 = 'NoProj1 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, project1);
|
||||
await createTimeEntryWithProject(page, project1, '1h');
|
||||
await createBareTimeEntry(page, 'Bare entry no project', '30min');
|
||||
const p1 = await createProjectViaApi(ctx, { name: project1 });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project1}`,
|
||||
duration: '1h',
|
||||
projectId: p1.id,
|
||||
});
|
||||
await createBareTimeEntryViaApi(ctx, 'Bare entry no project', '30min');
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText('Bare entry no project')).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText('Bare entry no project').first()).toBeVisible();
|
||||
|
||||
// Open project dropdown and select "No Project"
|
||||
await page.getByRole('button', { name: 'Projects' }).first().click();
|
||||
@@ -449,22 +538,31 @@ test('test that "No Project" filter shows entries without a project', async ({ p
|
||||
).toBeVisible();
|
||||
|
||||
// Only the bare entry (no project) should be visible
|
||||
await expect(page.getByText('Bare entry no project')).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`)).not.toBeVisible();
|
||||
await expect(page.getByText('Bare entry no project').first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('test that "No Task" filter shows entries without a task', async ({ page }) => {
|
||||
test('test that "No Task" filter shows entries without a task', async ({ page, ctx }) => {
|
||||
const projectName = 'NoTaskProj ' + Math.floor(Math.random() * 10000);
|
||||
const task1 = 'NoTaskFilter1 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, projectName);
|
||||
await createTask(page, projectName, task1);
|
||||
await createTimeEntryWithProjectAndTask(page, projectName, task1, '1h');
|
||||
await createTimeEntryWithProject(page, projectName, '30min');
|
||||
const project = await createProjectViaApi(ctx, { name: projectName });
|
||||
const t1 = await createTaskViaApi(ctx, { name: task1, project_id: project.id });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName} - ${task1}`,
|
||||
duration: '1h',
|
||||
projectId: project.id,
|
||||
taskId: t1.id,
|
||||
});
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectName}`,
|
||||
duration: '30min',
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName}`).first()).toBeVisible();
|
||||
|
||||
// Open task dropdown and select "No Task"
|
||||
@@ -476,19 +574,19 @@ test('test that "No Task" filter shows entries without a task', async ({ page })
|
||||
await expect(page.getByRole('button', { name: 'Tasks' }).first().getByText('1')).toBeVisible();
|
||||
|
||||
// Only the entry without a task should be visible
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`)).not.toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectName} - ${task1}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('test that "No Tag" filter shows entries without tags', async ({ page }) => {
|
||||
test('test that "No Tag" filter shows entries without tags', async ({ page, ctx }) => {
|
||||
const tag1 = 'NoTagFilter1 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createTimeEntryWithTag(page, tag1, '1h');
|
||||
await createBareTimeEntry(page, 'Entry without any tag', '30min');
|
||||
await createTimeEntryWithTagViaApi(ctx, tag1, '1h');
|
||||
await createBareTimeEntryViaApi(ctx, 'Entry without any tag', '30min');
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`)).toBeVisible();
|
||||
await expect(page.getByText('Entry without any tag')).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`).first()).toBeVisible();
|
||||
await expect(page.getByText('Entry without any tag').first()).toBeVisible();
|
||||
|
||||
// Open tag dropdown and select "No Tag"
|
||||
await page.getByRole('button', { name: 'Tags' }).click();
|
||||
@@ -498,25 +596,36 @@ test('test that "No Tag" filter shows entries without tags', async ({ page }) =>
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Tags' }).getByText('1')).toBeVisible();
|
||||
|
||||
await expect(page.getByText('Entry without any tag')).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`)).not.toBeVisible();
|
||||
await expect(page.getByText('Entry without any tag').first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry with tag ${tag1}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('test that "No Client" filter shows entries without a client', async ({ page }) => {
|
||||
test('test that "No Client" filter shows entries without a client', async ({ page, ctx }) => {
|
||||
const client1 = 'NoClientFilter ' + Math.floor(Math.random() * 10000);
|
||||
const projectWithClient = 'NoClientProj1 ' + Math.floor(Math.random() * 10000);
|
||||
const projectNoClient = 'NoClientProj2 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createClient(page, client1);
|
||||
await createProjectWithClient(page, projectWithClient, client1);
|
||||
await createProject(page, projectNoClient);
|
||||
await createTimeEntryWithProject(page, projectWithClient, '1h');
|
||||
await createTimeEntryWithProject(page, projectNoClient, '30min');
|
||||
const c1 = await createClientViaApi(ctx, { name: client1 });
|
||||
const pWithClient = await createProjectViaApi(ctx, {
|
||||
name: projectWithClient,
|
||||
client_id: c1.id,
|
||||
});
|
||||
const pNoClient = await createProjectViaApi(ctx, { name: projectNoClient });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectWithClient}`,
|
||||
duration: '1h',
|
||||
projectId: pWithClient.id,
|
||||
});
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${projectNoClient}`,
|
||||
duration: '30min',
|
||||
projectId: pNoClient.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${projectWithClient}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectNoClient}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectWithClient}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectNoClient}`).first()).toBeVisible();
|
||||
|
||||
// Open client dropdown and select "No Client"
|
||||
await page.getByRole('button', { name: 'Clients' }).first().click();
|
||||
@@ -528,21 +637,25 @@ test('test that "No Client" filter shows entries without a client', async ({ pag
|
||||
page.getByRole('button', { name: 'Clients' }).first().getByText('1')
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByText(`Entry for ${projectNoClient}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectWithClient}`)).not.toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectNoClient}`).first()).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${projectWithClient}`).first()).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('test that combining "No Project" with a project ID shows both', async ({ page }) => {
|
||||
test('test that combining "No Project" with a project ID shows both', async ({ page, ctx }) => {
|
||||
const project1 = 'CombNoProj ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, project1);
|
||||
await createTimeEntryWithProject(page, project1, '1h');
|
||||
await createBareTimeEntry(page, 'Bare combined entry', '30min');
|
||||
const p1 = await createProjectViaApi(ctx, { name: project1 });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project1}`,
|
||||
duration: '1h',
|
||||
projectId: p1.id,
|
||||
});
|
||||
await createBareTimeEntryViaApi(ctx, 'Bare combined entry', '30min');
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText('Bare combined entry')).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText('Bare combined entry').first()).toBeVisible();
|
||||
|
||||
// Select both "No Project" and the specific project
|
||||
await page.getByRole('button', { name: 'Projects' }).first().click();
|
||||
@@ -557,25 +670,33 @@ test('test that combining "No Project" with a project ID shows both', async ({ p
|
||||
).toBeVisible();
|
||||
|
||||
// Both entries should be visible
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText('Bare combined entry')).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
await expect(page.getByText('Bare combined entry').first()).toBeVisible();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Keyboard Navigation Tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
test('test that keyboard navigation works in multiselect dropdown', async ({ page }) => {
|
||||
test('test that keyboard navigation works in multiselect dropdown', async ({ page, ctx }) => {
|
||||
const project1 = 'KbNavProj1 ' + Math.floor(Math.random() * 10000);
|
||||
const project2 = 'KbNavProj2 ' + Math.floor(Math.random() * 10000);
|
||||
|
||||
await createProject(page, project1);
|
||||
await createProject(page, project2);
|
||||
await createTimeEntryWithProject(page, project1, '1h');
|
||||
await createTimeEntryWithProject(page, project2, '2h');
|
||||
const p1 = await createProjectViaApi(ctx, { name: project1 });
|
||||
const p2 = await createProjectViaApi(ctx, { name: project2 });
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project1}`,
|
||||
duration: '1h',
|
||||
projectId: p1.id,
|
||||
});
|
||||
await createTimeEntryViaApi(ctx, {
|
||||
description: `Entry for ${project2}`,
|
||||
duration: '2h',
|
||||
projectId: p2.id,
|
||||
});
|
||||
|
||||
await goToReportingDetailed(page);
|
||||
await expect(page.getByText(`Entry for ${project1}`)).toBeVisible();
|
||||
await expect(page.getByText(`Entry for ${project1}`).first()).toBeVisible();
|
||||
|
||||
// Open project dropdown
|
||||
await page.getByRole('button', { name: 'Projects' }).first().click();
|
||||
|
||||
Reference in New Issue
Block a user