import { expect } from '@playwright/test'; import type { Page } from '@playwright/test'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; import { test } from '../playwright/fixtures'; import { createProjectViaApi, createPublicProjectViaApi, createTaskViaApi, createClientViaApi, updateOrganizationSettingViaApi, } from './utils/api'; async function goToProjectsOverview(page: Page) { await page.goto(PLAYWRIGHT_BASE_URL + '/projects'); } test('test that creating and deleting a new task in a new project works', async ({ page }) => { const newProjectName = 'New 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); 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.id !== null && (await response.json()).data.color !== null && (await response.json()).data.client_id === null && (await response.json()).data.name === newProjectName ), ]); await expect(page.getByTestId('project_table')).toContainText(newProjectName); await page.getByText(newProjectName).click(); const newTaskName = 'New Task ' + Math.floor(1 + Math.random() * 10000); await page.getByRole('button', { name: 'Create Task' }).click(); await page.getByPlaceholder('Task Name').fill(newTaskName); await Promise.all([ page.getByRole('button', { name: 'Create Task' }).click(), page.waitForResponse( async (response) => response.url().includes('/tasks') && response.request().method() === 'POST' && response.status() === 201 && (await response.json()).data.id !== null && (await response.json()).data.project_id !== null && (await response.json()).data.name === newTaskName ), ]); await expect(page.getByTestId('task_table')).toContainText(newTaskName); const taskMoreButton = page.locator("[aria-label='Actions for Task " + newTaskName + "']"); taskMoreButton.click(); const taskDeleteButton = page.locator("[aria-label='Delete Task " + newTaskName + "']"); await Promise.all([ taskDeleteButton.click(), page.waitForResponse( async (response) => response.url().includes('/tasks') && response.request().method() === 'DELETE' && response.status() === 204 ), ]); await expect(page.getByTestId('task_table')).not.toContainText(newTaskName); await goToProjectsOverview(page); const moreButton = page.locator("[aria-label='Actions for Project " + newProjectName + "']"); moreButton.click(); const deleteButton = page.locator("[aria-label='Delete Project " + newProjectName + "']"); await Promise.all([ deleteButton.click(), page.waitForResponse( async (response) => response.url().includes('/projects') && response.request().method() === 'DELETE' && response.status() === 204 ), ]); await expect(page.getByTestId('project_table')).not.toContainText(newProjectName); }); test('test that archiving and unarchiving tasks works', async ({ page, ctx }) => { const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000); const newTaskName = 'New Task ' + Math.floor(1 + Math.random() * 10000); const project = await createProjectViaApi(ctx, { name: newProjectName }); await createTaskViaApi(ctx, { name: newTaskName, project_id: project.id }); await page.goto(PLAYWRIGHT_BASE_URL + '/projects/' + project.id); await expect(page.getByRole('table')).toContainText(newTaskName); await page.getByRole('row').first().getByRole('button').click(); await Promise.all([ page.getByRole('menuitem').getByText('Mark as done').first().click(), expect(page.getByText(newTaskName)).not.toBeVisible(), ]); await Promise.all([ page.getByRole('tab', { name: 'Done' }).click(), expect(page.getByText(newTaskName)).toBeVisible(), ]); await page.getByRole('row').first().getByRole('button').click(); await Promise.all([ page.getByRole('menuitem').getByText('Mark as active').first().click(), expect(page.getByText(newTaskName)).not.toBeVisible(), ]); await Promise.all([ page.getByRole('tab', { name: 'Active' }).click(), expect(page.getByText(newTaskName)).toBeVisible(), ]); }); test('test that editing a task name works', async ({ page, ctx }) => { const projectName = 'TaskEdit Project ' + Math.floor(1 + Math.random() * 10000); const originalTaskName = 'Original Task ' + Math.floor(1 + Math.random() * 10000); const updatedTaskName = 'Updated Task ' + Math.floor(1 + Math.random() * 10000); const project = await createProjectViaApi(ctx, { name: projectName }); await createTaskViaApi(ctx, { name: originalTaskName, project_id: project.id }); await page.goto(PLAYWRIGHT_BASE_URL + '/projects/' + project.id); await expect(page.getByTestId('task_table')).toContainText(originalTaskName); // Open actions menu and click Edit const moreButton = page.locator("[aria-label='Actions for Task " + originalTaskName + "']"); await moreButton.click(); await page.getByRole('menuitem').getByText('Edit').click(); // Update the task name await expect(page.getByRole('dialog')).toBeVisible(); await page.getByPlaceholder('Task Name').fill(updatedTaskName); await Promise.all([ page.getByRole('button', { name: 'Update Task' }).click(), page.waitForResponse( (response) => response.url().includes('/tasks') && response.request().method() === 'PUT' && response.status() === 200 ), ]); await expect(page.getByTestId('task_table')).toContainText(updatedTaskName); await expect(page.getByTestId('task_table')).not.toContainText(originalTaskName); }); test('test that the project can be searched and changed in the create task modal', async ({ page, ctx, }) => { const sourceProject = 'Source Project ' + Math.floor(1 + Math.random() * 100000); const targetProject = 'Target Project ' + Math.floor(1 + Math.random() * 100000); await createProjectViaApi(ctx, { name: sourceProject }); const target = await createProjectViaApi(ctx, { name: targetProject }); await goToProjectsOverview(page); await page.getByText(sourceProject).first().click(); await page.getByRole('button', { name: 'Create Task' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); // The project dropdown is pre-filled with the source project; open it. await page.getByRole('dialog').getByRole('button', { name: sourceProject }).click(); // Opening the dropdown focuses the search input; searching narrows it to the target project. const projectSearch = page.getByPlaceholder('Search for a project...'); await expect(projectSearch).toBeFocused(); await projectSearch.fill('Target Project'); await page.getByRole('option', { name: targetProject }).click(); // Selecting closes the dropdown and updates the trigger to the chosen project. await expect( page.getByRole('dialog').getByRole('button', { name: targetProject }) ).toBeVisible(); // The new selection is what gets used when the task is created. const taskName = 'Switched Task ' + Math.floor(1 + Math.random() * 100000); await page.getByPlaceholder('Task Name').fill(taskName); await Promise.all([ page.getByRole('dialog').getByRole('button', { name: 'Create Task' }).click(), page.waitForResponse( async (response) => response.url().includes('/tasks') && response.request().method() === 'POST' && response.status() === 201 && (await response.json()).data.project_id === target.id ), ]); }); test('test that creating a project with an existing client works', async ({ page, ctx }) => { const clientName = 'Existing Client ' + Math.floor(1 + Math.random() * 10000); const projectName = 'Project With Client ' + Math.floor(1 + Math.random() * 10000); await createClientViaApi(ctx, { name: clientName }); await goToProjectsOverview(page); await page.getByRole('button', { name: 'Create Project' }).click(); await page.getByLabel('Project Name').fill(projectName); // Select the existing client await page.getByRole('dialog').getByRole('button', { name: 'No Client' }).click(); await page.getByRole('option', { name: clientName }).click(); await Promise.all([ page.getByRole('dialog').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.client_id !== null ), ]); await expect(page.getByTestId('project_table')).toContainText(projectName); await expect(page.getByTestId('project_table')).toContainText(clientName); }); test('test that multiple tasks are displayed on project detail page', async ({ page, ctx }) => { const projectName = 'TaskCount Project ' + Math.floor(1 + Math.random() * 10000); const taskName1 = 'CountTask A ' + Math.floor(1 + Math.random() * 10000); const taskName2 = 'CountTask B ' + Math.floor(1 + Math.random() * 10000); const project = await createProjectViaApi(ctx, { name: projectName }); await createTaskViaApi(ctx, { name: taskName1, project_id: project.id }); await createTaskViaApi(ctx, { name: taskName2, project_id: project.id }); await page.goto(PLAYWRIGHT_BASE_URL + '/projects/' + project.id); await expect(page.getByText(taskName1)).toBeVisible(); 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 // ============================================= test.describe('Employee Tasks Restrictions', () => { test('employee cannot see task management actions when employees_can_manage_tasks is disabled', async ({ ctx, employee, }) => { // Create a public project with a task const projectName = 'EmpTaskProj ' + Math.floor(Math.random() * 10000); const taskName = 'EmpTask ' + Math.floor(Math.random() * 10000); const project = await createPublicProjectViaApi(ctx, { name: projectName }); await createTaskViaApi(ctx, { name: taskName, project_id: project.id }); // Navigate to the project detail page await employee.page.goto(PLAYWRIGHT_BASE_URL + '/projects'); await expect(employee.page.getByText(projectName)).toBeVisible({ timeout: 10000 }); await employee.page.getByText(projectName).first().click(); await employee.page.waitForURL(/\/projects\/[a-f0-9-]+/); // Task should be visible await expect(employee.page.getByText(taskName)).toBeVisible({ timeout: 10000 }); // Create Task button should not be visible await expect(employee.page.getByRole('button', { name: 'Create Task' })).not.toBeVisible(); // Task actions button should not be visible const actionsButton = employee.page.locator(`[aria-label='Actions for Task ${taskName}']`); await expect(actionsButton).not.toBeVisible(); }); test('employee can manage tasks when employees_can_manage_tasks is enabled', async ({ ctx, employee, }) => { // Enable the setting await updateOrganizationSettingViaApi(ctx, { employees_can_manage_tasks: true }); const projectName = 'EmpTaskMgmtProj ' + Math.floor(Math.random() * 10000); await createPublicProjectViaApi(ctx, { name: projectName }); // Navigate to the project detail page await employee.page.goto(PLAYWRIGHT_BASE_URL + '/projects'); await expect(employee.page.getByText(projectName)).toBeVisible({ timeout: 10000 }); await employee.page.getByText(projectName).first().click(); await employee.page.waitForURL(/\/projects\/[a-f0-9-]+/); // Create Task button SHOULD be visible await expect(employee.page.getByRole('button', { name: 'Create Task' })).toBeVisible(); }); });