diff --git a/.gitignore b/.gitignore index c23d8c0..d3a3665 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ public/assets/ # Webpack cache .webpack/ + +# Playwright test results +test-results/ +.last-run.json diff --git a/e2e/tests/area.spec.ts b/e2e/tests/area.spec.ts new file mode 100644 index 0000000..08572cb --- /dev/null +++ b/e2e/tests/area.spec.ts @@ -0,0 +1,146 @@ +import { test, expect } from '@playwright/test'; + +// Shared login function +async function loginAndNavigateToAreas(page, baseURL) { + const appUrl = baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + + // Go directly to login page first + await page.goto(appUrl + '/login'); + + // Fill credentials and login + const email = process.env.E2E_EMAIL || 'test@tududi.com'; + const password = process.env.E2E_PASSWORD || 'password123'; + + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: /login/i }).click(); + + // Wait for redirect to Today view + await expect(page).toHaveURL(/\/today$/); + + // Navigate to areas page + await page.goto(appUrl + '/areas'); + await expect(page).toHaveURL(/\/areas/); + + return appUrl; +} + +// Shared function to create an area via the sidebar button +async function createArea(page, areaName, areaDescription = '') { + // Find the "Add Area" button in the sidebar + const addAreaButton = page.locator('button[aria-label*="Area"]'); + await expect(addAreaButton).toBeVisible(); + + // Click the Add Area button + await addAreaButton.click(); + + // Wait for the Area Modal to appear + await expect(page.locator('input[name="name"]')).toBeVisible({ timeout: 10000 }); + + // Fill in the area name + await page.locator('input[name="name"]').fill(areaName); + + // Fill in the area description if provided + if (areaDescription) { + await page.locator('textarea[name="description"]').fill(areaDescription); + } + + // Save the area + await page.getByRole('button', { name: /create.*area|save/i }).click(); + + // Wait for the modal to close + await expect(page.locator('input[name="name"]')).not.toBeVisible({ timeout: 10000 }); + + // Wait for area creation to complete + await page.waitForTimeout(2000); +} + +test('user can create a new area and verify it appears in the areas list', async ({ page, baseURL }) => { + await loginAndNavigateToAreas(page, baseURL); + + // Create a unique test area + const timestamp = Date.now(); + const areaName = `Test Area ${timestamp}`; + const areaDescription = `This is test description for area ${timestamp}`; + await createArea(page, areaName, areaDescription); + + // Verify the area appears in the areas list + await expect(page.getByText(areaName)).toBeVisible({ timeout: 10000 }); +}); + +test('user can update an existing area', async ({ page, baseURL }) => { + await loginAndNavigateToAreas(page, baseURL); + + // Create an initial area + const timestamp = Date.now(); + const originalAreaName = `Test area to edit ${timestamp}`; + const originalAreaDescription = `Original description ${timestamp}`; + await createArea(page, originalAreaName, originalAreaDescription); + + // Find the area container and hover to show dropdown + const areaContainer = page.getByText(originalAreaName).locator('..'); + await areaContainer.hover(); + + // Click the three dots menu + await areaContainer.locator('button[aria-label*="dropdown"]').click(); + + // Click Edit in the dropdown + await page.getByText('Edit').click(); + + // Wait for the Area Modal to appear with the area data + await expect(page.locator('input[name="name"]')).toBeVisible(); + + // Verify the area name field is pre-filled + const areaNameInput = page.locator('input[name="name"]').first(); + await expect(areaNameInput).toHaveValue(originalAreaName); + + // Edit the area name and description + const editedAreaName = `Edited test area ${timestamp}`; + const editedAreaDescription = `Edited description ${timestamp}`; + await areaNameInput.clear(); + await areaNameInput.fill(editedAreaName); + + const areaDescriptionTextarea = page.locator('textarea[name="description"]').first(); + await areaDescriptionTextarea.clear(); + await areaDescriptionTextarea.fill(editedAreaDescription); + + // Save the changes + await page.getByRole('button', { name: /save|update/i }).click(); + + // Wait for the modal to close + await expect(page.locator('input[name="name"]')).not.toBeVisible(); + + // Verify the edited area appears in the areas list + await expect(page.getByText(editedAreaName)).toBeVisible(); + + // Verify the original area name is no longer visible + await expect(page.getByText(originalAreaName)).not.toBeVisible(); +}); + +test('user can delete an existing area', async ({ page, baseURL }) => { + await loginAndNavigateToAreas(page, baseURL); + + // Create an initial area + const timestamp = Date.now(); + const areaName = `Test area to delete ${timestamp}`; + const areaDescription = `Description to delete ${timestamp}`; + await createArea(page, areaName, areaDescription); + + // Find the area container and hover to show dropdown + const areaContainer = page.getByText(areaName).locator('..'); + await areaContainer.hover(); + + // Click the three dots menu + await areaContainer.locator('button[aria-label*="dropdown"]').click(); + + // Click Delete in the dropdown + await page.getByText('Delete').click(); + + // Wait for and handle the confirmation dialog + await expect(page.locator('text=Delete Area')).toBeVisible(); + // Click the confirm button in the confirmation dialog + await page.getByRole('button', { name: /confirm|delete/i }).click(); + + // Verify the area is no longer visible in the areas list + await expect(page.getByText(areaName)).not.toBeVisible(); +}); \ No newline at end of file diff --git a/e2e/tests/inbox.spec.ts b/e2e/tests/inbox.spec.ts new file mode 100644 index 0000000..925f424 --- /dev/null +++ b/e2e/tests/inbox.spec.ts @@ -0,0 +1,239 @@ +import { test, expect } from '@playwright/test'; + +// Shared login function +async function loginAndNavigateToInbox(page, baseURL) { + const appUrl = baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + + // Go directly to login page first + await page.goto(appUrl + '/login'); + + // Fill credentials and login + const email = process.env.E2E_EMAIL || 'test@tududi.com'; + const password = process.env.E2E_PASSWORD || 'password123'; + + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: /login/i }).click(); + + // Wait for redirect to Today view + await expect(page).toHaveURL(/\/today$/); + + // Navigate to inbox page + await page.goto(appUrl + '/inbox'); + await expect(page).toHaveURL(/\/inbox$/); + + return appUrl; +} + +// Shared function to create an inbox item +async function createInboxItem(page, content) { + // Click the Quick Inbox Capture button in the navbar + await page.getByRole('button', { name: 'Quick Inbox Capture' }).click(); + + // Wait for the InboxModal to appear + await expect(page.locator('input[name="text"]')).toBeVisible(); + + // Add the test item + await page.locator('input[name="text"]').fill(content); + + // Submit the form by pressing Enter + await page.locator('input[name="text"]').press('Enter'); + + // Wait for the modal to close + await expect(page.locator('input[name="text"]')).not.toBeVisible(); + + // Verify the item appears in the inbox list + await expect(page.locator('text=' + content)).toBeVisible(); +} + +test('user can add a new inbox item and verify it has been added', async ({ page, baseURL }) => { + await loginAndNavigateToInbox(page, baseURL); + + // Add a unique test item + const testContent = `Test inbox item ${Date.now()}`; + await createInboxItem(page, testContent); +}); + +test('user can edit an inbox item', async ({ page, baseURL }) => { + await loginAndNavigateToInbox(page, baseURL); + + // Create an initial item + const timestamp = Date.now(); + const originalContent = `Test item to edit ${timestamp}`; + await createInboxItem(page, originalContent); + + // Find the inbox item container and hover to show edit button + const inboxItemContainer = page.locator('.rounded-lg.shadow-sm').filter({ hasText: originalContent }); + await inboxItemContainer.hover(); + + // Click the edit button (pencil icon) - it has title="Edit" + await inboxItemContainer.locator('button[title="Edit"]').click(); + + // Wait for the edit modal to appear + await expect(page.locator('input[name="text"]')).toBeVisible(); + + // Edit the content + const editedContent = `Edited test item ${timestamp}`; + await page.locator('input[name="text"]').clear(); + await page.locator('input[name="text"]').fill(editedContent); + await page.locator('input[name="text"]').press('Enter'); + + // Wait for the modal to close + await expect(page.locator('input[name="text"]')).not.toBeVisible(); + + // Verify the edited content appears + await expect(page.locator('text=' + editedContent)).toBeVisible(); + + // Verify the original content is no longer visible + await expect(page.locator('text=' + originalContent)).not.toBeVisible(); +}); + +test('user can delete an inbox item', async ({ page, baseURL }) => { + await loginAndNavigateToInbox(page, baseURL); + + // Create an initial item + const timestamp = Date.now(); + const testContent = `Test item to delete ${timestamp}`; + await createInboxItem(page, testContent); + + // Find the inbox item container and hover to show delete button + const inboxItemContainer = page.locator('.rounded-lg.shadow-sm').filter({ hasText: testContent }); + await inboxItemContainer.hover(); + + // Click the delete button (trash icon) - it has title="Delete" + await inboxItemContainer.locator('button[title="Delete"]').click(); + + // Wait for and handle the confirmation dialog + await expect(page.locator('text=Delete Item')).toBeVisible(); + // Click the red "Delete" button in the confirmation dialog + await page.locator('.bg-red-500.text-white').click(); + + // Verify the item is no longer visible + await expect(page.locator('text=' + testContent)).not.toBeVisible(); +}); + +test('user can create task from inbox item', async ({ page, baseURL }) => { + const appUrl = await loginAndNavigateToInbox(page, baseURL); + + // Create an initial item + const timestamp = Date.now(); + const testContent = `Test item to convert to task ${timestamp}`; + await createInboxItem(page, testContent); + + // Find the inbox item container and hover to show convert buttons + const inboxItemContainer = page.locator('.rounded-lg.shadow-sm').filter({ hasText: testContent }); + await inboxItemContainer.hover(); + + // Click the "Convert to Task" button (clipboard icon with title="Create task") + await inboxItemContainer.locator('button[title="Create task"]').click(); + + // Wait for the Task Modal to appear + await expect(page.locator('input[name="name"], input[placeholder*="task" i], input[placeholder*="name" i]')).toBeVisible({ timeout: 10000 }); + + // Verify the task name field is pre-filled with the inbox item content + const taskNameInput = page.locator('input[name="name"], input[placeholder*="task" i], input[placeholder*="name" i]').first(); + await expect(taskNameInput).toHaveValue(testContent); + + // Save the task - Use a more specific selector within the modal + await page.locator('.bg-blue-600.text-white').filter({ hasText: 'Save' }).click(); + + // Wait for success message or modal to close + await expect(page.locator('input[name="name"], input[placeholder*="task" i], input[placeholder*="name" i]')).not.toBeVisible({ timeout: 10000 }); + + // Navigate back to inbox to verify the item was processed + await page.goto(appUrl + '/inbox'); + + // Verify the original inbox item is no longer in the inbox (successfully converted to task) + await expect(page.locator('.rounded-lg.shadow-sm').filter({ hasText: testContent })).not.toBeVisible(); + + // Navigate to tasks page to verify the task was created there + await page.goto(appUrl + '/tasks'); + await expect(page).toHaveURL(/\/tasks$/); + + // Verify the created task appears in the tasks list using the task-item-wrapper class + await expect(page.locator('.task-item-wrapper').filter({ hasText: testContent })).toBeVisible(); +}); + +test('user can create project from inbox item', async ({ page, baseURL }) => { + const appUrl = await loginAndNavigateToInbox(page, baseURL); + + // Create an initial item + const timestamp = Date.now(); + const testContent = `Test project from inbox ${timestamp}`; + await createInboxItem(page, testContent); + + // Find the inbox item container and hover to show convert buttons + const inboxItemContainer = page.locator('.rounded-lg.shadow-sm').filter({ hasText: testContent }); + await inboxItemContainer.hover(); + + // Click the "Create project" button + await inboxItemContainer.locator('button[title="Create project"]').click(); + + // Wait for the Project Modal to appear + await expect(page.locator('input[name="name"], input[placeholder*="project" i], input[placeholder*="name" i]')).toBeVisible({ timeout: 10000 }); + + // Verify the project name field is pre-filled with the inbox item content + const projectNameInput = page.locator('input[name="name"], input[placeholder*="project" i], input[placeholder*="name" i]').first(); + await expect(projectNameInput).toHaveValue(testContent); + + // Save the project - Use a more generic approach for the submit button + await page.getByRole('button', { name: /create.*project|save/i }).click(); + + // Wait for success message or modal to close + await expect(page.locator('input[name="name"], input[placeholder*="project" i], input[placeholder*="name" i]')).not.toBeVisible({ timeout: 10000 }); + + // Navigate back to inbox to verify the item was processed + await page.goto(appUrl + '/inbox'); + + // Verify the original inbox item is no longer in the inbox (successfully converted to project) + await expect(page.locator('.rounded-lg.shadow-sm').filter({ hasText: testContent })).not.toBeVisible(); + + // Navigate to projects page to verify the project was created there + await page.goto(appUrl + '/projects'); + await expect(page).toHaveURL(/\/projects$/); + + // Verify the created project appears in the projects list + await expect(page.locator('text=' + testContent)).toBeVisible(); +}); + +test('user can create note from inbox item', async ({ page, baseURL }) => { + const appUrl = await loginAndNavigateToInbox(page, baseURL); + + // Create an initial item + const timestamp = Date.now(); + const testContent = `Test note from inbox ${timestamp}`; + await createInboxItem(page, testContent); + + // Find the inbox item container and hover to show convert buttons + const inboxItemContainer = page.locator('.rounded-lg.shadow-sm').filter({ hasText: testContent }); + await inboxItemContainer.hover(); + + // Click the "Create note" button + await inboxItemContainer.locator('button[title="Create note"]').click(); + + // Wait for the Note Modal to appear + await expect(page.locator('input[name="title"], input[placeholder*="note" i], input[placeholder*="title" i]')).toBeVisible({ timeout: 10000 }); + + // Verify the note title field is pre-filled with the inbox item content + const noteTitleInput = page.locator('input[name="title"], input[placeholder*="note" i], input[placeholder*="title" i]').first(); + await expect(noteTitleInput).toHaveValue(testContent); + + // Save the note - Use a more generic approach for the submit button + await page.getByRole('button', { name: /create.*note|save/i }).click(); + + // Wait for success message or modal to close + await expect(page.locator('input[name="title"], input[placeholder*="note" i], input[placeholder*="title" i]')).not.toBeVisible({ timeout: 10000 }); + + // Navigate back to inbox to verify the item was processed + await page.goto(appUrl + '/inbox'); + + // Verify the original inbox item is no longer in the inbox (successfully converted to note) + await expect(page.locator('.rounded-lg.shadow-sm').filter({ hasText: testContent })).not.toBeVisible(); + + // Navigate to notes page to verify the note was created there + await page.goto(appUrl + '/notes'); + await expect(page).toHaveURL(/\/notes$/); + + // Verify the created note appears in the notes list - use a more specific selector to avoid strict mode + await expect(page.locator('.note-item, .rounded-lg, .border').filter({ hasText: testContent })).toBeVisible(); +}); \ No newline at end of file diff --git a/e2e/tests/note.spec.ts b/e2e/tests/note.spec.ts new file mode 100644 index 0000000..20ca561 --- /dev/null +++ b/e2e/tests/note.spec.ts @@ -0,0 +1,140 @@ +import { test, expect } from '@playwright/test'; + +// Shared login function +async function loginAndNavigateToNotes(page, baseURL) { + const appUrl = baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + + // Go directly to login page first + await page.goto(appUrl + '/login'); + + // Fill credentials and login + const email = process.env.E2E_EMAIL || 'test@tududi.com'; + const password = process.env.E2E_PASSWORD || 'password123'; + + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: /login/i }).click(); + + // Wait for redirect to Today view + await expect(page).toHaveURL(/\/today$/); + + // Navigate to notes page + await page.goto(appUrl + '/notes'); + await expect(page).toHaveURL(/\/notes/); + + return appUrl; +} + +// Shared function to create a note via the sidebar button +async function createNote(page, noteTitle, noteContent = '') { + // Find the "Add Note" button in the sidebar + const addNoteButton = page.locator('button[aria-label="Add Note"]'); + await expect(addNoteButton).toBeVisible(); + + // Click the Add Note button + await addNoteButton.click(); + + // Wait for the Note Modal to appear + await expect(page.locator('input[name="title"]')).toBeVisible({ timeout: 10000 }); + + // Fill in the note title + await page.locator('input[name="title"]').fill(noteTitle); + + // Fill in the note content if provided + if (noteContent) { + await page.locator('textarea[name="content"]').fill(noteContent); + } + + // Save the note + await page.getByRole('button', { name: /create.*note|save/i }).click(); + + // Wait for the modal to close + await expect(page.locator('input[name="title"]')).not.toBeVisible({ timeout: 10000 }); + + // Wait for note creation to complete + await page.waitForTimeout(2000); +} + +test('user can create a new note and verify it appears in the notes list', async ({ page, baseURL }) => { + await loginAndNavigateToNotes(page, baseURL); + + // Create a unique test note + const timestamp = Date.now(); + const noteTitle = `Test Note ${timestamp}`; + const noteContent = `This is test content for note ${timestamp}`; + await createNote(page, noteTitle, noteContent); + + // Verify the note appears in the notes list + await expect(page.getByText(noteTitle)).toBeVisible({ timeout: 10000 }); +}); + +test('user can update an existing note', async ({ page, baseURL }) => { + await loginAndNavigateToNotes(page, baseURL); + + // Create an initial note + const timestamp = Date.now(); + const originalNoteTitle = `Test note to edit ${timestamp}`; + const originalNoteContent = `Original content ${timestamp}`; + await createNote(page, originalNoteTitle, originalNoteContent); + + // Find and click the note to edit it + const noteContainer = page.getByText(originalNoteTitle).locator('..'); + await noteContainer.hover(); + + // Click the edit button (pencil icon) + await noteContainer.locator('button[title="Edit"], button').filter({ hasText: '' }).first().click(); + + // Wait for the Note Modal to appear with the note data + await expect(page.locator('input[name="title"]')).toBeVisible(); + + // Verify the note title field is pre-filled + const noteTitleInput = page.locator('input[name="title"]').first(); + await expect(noteTitleInput).toHaveValue(originalNoteTitle); + + // Edit the note title and content + const editedNoteTitle = `Edited test note ${timestamp}`; + const editedNoteContent = `Edited content ${timestamp}`; + await noteTitleInput.clear(); + await noteTitleInput.fill(editedNoteTitle); + + const noteContentTextarea = page.locator('textarea[name="content"]').first(); + await noteContentTextarea.clear(); + await noteContentTextarea.fill(editedNoteContent); + + // Save the changes + await page.getByRole('button', { name: /save/i }).click(); + + // Wait for the modal to close + await expect(page.locator('input[name="title"]')).not.toBeVisible(); + + // Verify the edited note appears in the notes list + await expect(page.getByText(editedNoteTitle)).toBeVisible(); + + // Verify the original note title is no longer visible + await expect(page.getByText(originalNoteTitle)).not.toBeVisible(); +}); + +test('user can delete an existing note', async ({ page, baseURL }) => { + await loginAndNavigateToNotes(page, baseURL); + + // Create an initial note + const timestamp = Date.now(); + const noteTitle = `Test note to delete ${timestamp}`; + const noteContent = `Content to delete ${timestamp}`; + await createNote(page, noteTitle, noteContent); + + // Find the note container and hover to show action buttons + const noteContainer = page.getByText(noteTitle).locator('..'); + await noteContainer.hover(); + + // Click the delete button (trash icon) + await noteContainer.locator('button[title="Delete"], button').filter({ hasText: '' }).last().click(); + + // Wait for and handle the confirmation dialog + await expect(page.locator('text=Delete Note')).toBeVisible(); + // Click the red "Delete" button in the confirmation dialog + await page.locator('.bg-red-500.text-white').click(); + + // Verify the note is no longer visible in the notes list + await expect(page.getByText(noteTitle)).not.toBeVisible(); +}); \ No newline at end of file diff --git a/e2e/tests/project.spec.ts b/e2e/tests/project.spec.ts new file mode 100644 index 0000000..ad7ebd3 --- /dev/null +++ b/e2e/tests/project.spec.ts @@ -0,0 +1,218 @@ +import { test, expect } from '@playwright/test'; + +// Shared login function +async function loginAndNavigateToProjects(page, baseURL) { + const appUrl = baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + + // Go directly to login page first + await page.goto(appUrl + '/login'); + + // Fill credentials and login + const email = process.env.E2E_EMAIL || 'test@tududi.com'; + const password = process.env.E2E_PASSWORD || 'password123'; + + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: /login/i }).click(); + + // Wait for redirect to Today view + await expect(page).toHaveURL(/\/today$/); + + // Navigate to projects page + await page.goto(appUrl + '/projects'); + await expect(page).toHaveURL(/\/projects/); + + return appUrl; +} + +// Shared function to create a project via the sidebar button +async function createProject(page, projectName) { + // Find the "Add Project" button in the sidebar + const addProjectButton = page.locator('button[aria-label="Add Project"]'); + await expect(addProjectButton).toBeVisible(); + + // Click the Add Project button + await addProjectButton.click(); + + // Wait for the Project Modal to appear + await expect(page.locator('input[name="name"]')).toBeVisible({ timeout: 10000 }); + + // Fill in the project name + await page.locator('input[name="name"]').fill(projectName); + + // Save the project + await page.getByRole('button', { name: /create.*project|save/i }).click(); + + // Wait for the modal to close + await expect(page.locator('input[name="name"]')).not.toBeVisible({ timeout: 10000 }); + + // Wait for project creation to complete + await page.waitForTimeout(2000); +} + +test('user can create a new project and verify it appears in the projects list', async ({ page, baseURL }) => { + await loginAndNavigateToProjects(page, baseURL); + + // Create a unique test project + const timestamp = Date.now(); + const projectName = `Test Project ${timestamp}`; + await createProject(page, projectName); + + // Verify the project appears in the projects list + await expect(page.getByText(projectName)).toBeVisible({ timeout: 10000 }); +}); + +test('user can update an existing project', async ({ page, baseURL }) => { + await loginAndNavigateToProjects(page, baseURL); + + // Create an initial project + const timestamp = Date.now(); + const originalProjectName = `Test project to edit ${timestamp}`; + await createProject(page, originalProjectName); + + // Find and click the project to open its details or edit it + // Look for the project card/item and find its edit button + const projectContainer = page.getByText(originalProjectName).locator('..'); + await projectContainer.hover(); + + // Click the edit button (pencil icon) + await projectContainer.locator('button[title="Edit"], button').filter({ hasText: '' }).first().click(); + + // Wait for the Project Modal to appear with the project data + await expect(page.locator('input[name="name"]')).toBeVisible(); + + // Verify the project name field is pre-filled + const projectNameInput = page.locator('input[name="name"]').first(); + await expect(projectNameInput).toHaveValue(originalProjectName); + + // Edit the project name + const editedProjectName = `Edited test project ${timestamp}`; + await projectNameInput.clear(); + await projectNameInput.fill(editedProjectName); + + // Save the changes + await page.getByRole('button', { name: /save/i }).click(); + + // Wait for the modal to close + await expect(page.locator('input[name="name"]')).not.toBeVisible(); + + // Verify the edited project appears in the projects list + await expect(page.getByText(editedProjectName)).toBeVisible(); + + // Verify the original project name is no longer visible + await expect(page.getByText(originalProjectName)).not.toBeVisible(); +}); + +test('user can delete an existing project', async ({ page, baseURL }) => { + await loginAndNavigateToProjects(page, baseURL); + + // Create an initial project + const timestamp = Date.now(); + const projectName = `Test project to delete ${timestamp}`; + await createProject(page, projectName); + + // Find the project container and hover to show action buttons + const projectContainer = page.getByText(projectName).locator('..'); + await projectContainer.hover(); + + // Click the delete button (trash icon) + await projectContainer.locator('button[title="Delete"], button').filter({ hasText: '' }).last().click(); + + // Wait for and handle the confirmation dialog + await expect(page.locator('text=Delete Project')).toBeVisible(); + // Click the red "Delete" button in the confirmation dialog + await page.locator('.bg-red-500.text-white').click(); + + // Verify the project is no longer visible in the projects list + await expect(page.getByText(projectName)).not.toBeVisible(); +}); + +test('user can add a task to a project via ProjectDetails view', async ({ page, baseURL }) => { + await loginAndNavigateToProjects(page, baseURL); + + // Create an initial project + const timestamp = Date.now(); + const projectName = `Test project for tasks ${timestamp}`; + await createProject(page, projectName); + + // Click on the project to open its details view + await page.getByText(projectName).click(); + + // Wait for the project details page to load + await expect(page).toHaveURL(/\/project\//); + + // Find the task creation input field within the project details + const taskInput = page.locator('input[placeholder="Προσθήκη Νέας Εργασίας"]').first(); + + // Wait for the input to be visible + await expect(taskInput).toBeVisible({ timeout: 5000 }); + + // Create a task within this project + const taskName = `Test task in project ${timestamp}`; + await taskInput.fill(taskName); + await taskInput.press('Enter'); + + // Verify task creation by checking that the input field is cleared + await expect(taskInput).toHaveValue(''); + + // Wait for the task to be created and appear in the project's task list + await page.waitForTimeout(2000); + + // Verify the task appears in the project's task list + // Use a more general approach since the exact structure might vary + await expect(page.getByText(taskName)).toBeVisible({ timeout: 10000 }); +}); + +test('user can delete a project with tasks - tasks should survive', async ({ page, baseURL }) => { + const appUrl = await loginAndNavigateToProjects(page, baseURL); + + // Create an initial project + const timestamp = Date.now(); + const projectName = `Test project with tasks ${timestamp}`; + await createProject(page, projectName); + + // Click on the project to open its details view + await page.getByText(projectName).click(); + + // Wait for the project details page to load + await expect(page).toHaveURL(/\/project\//); + + // Add a task to this project + const taskInput = page.locator('input[placeholder="Προσθήκη Νέας Εργασίας"]').first(); + await expect(taskInput).toBeVisible({ timeout: 5000 }); + + const taskName = `Task that should survive project deletion ${timestamp}`; + await taskInput.fill(taskName); + await taskInput.press('Enter'); + await expect(taskInput).toHaveValue(''); + await page.waitForTimeout(2000); + + // Navigate back to projects list + await page.goto(appUrl + '/projects'); + await expect(page).toHaveURL(/\/projects/); + + // Delete the project + const projectContainer = page.getByText(projectName).locator('..'); + await projectContainer.hover(); + await projectContainer.locator('button[title="Delete"], button').filter({ hasText: '' }).last().click(); + + // Handle the confirmation dialog + await expect(page.locator('text=Delete Project')).toBeVisible(); + await page.locator('.bg-red-500.text-white').click(); + + // Verify the project is deleted + await expect(page.getByText(projectName)).not.toBeVisible(); + + // Verify the task still exists - navigate to tasks page + await page.goto(appUrl + '/tasks'); + await expect(page).toHaveURL(/\/tasks/); + + // Wait for tasks to load + await page.waitForTimeout(2000); + + // The task should still exist but without the project association + // This is the expected behavior based on backend implementation: + // - project.destroy() doesn't cascade to tasks + // - tasks have project_id set to NULL when project is deleted + await expect(page.getByText(taskName)).toBeVisible({ timeout: 10000 }); +}); \ No newline at end of file diff --git a/e2e/tests/tag.spec.ts b/e2e/tests/tag.spec.ts new file mode 100644 index 0000000..4e983b6 --- /dev/null +++ b/e2e/tests/tag.spec.ts @@ -0,0 +1,127 @@ +import { test, expect } from '@playwright/test'; + +// Shared login function +async function loginAndNavigateToTags(page, baseURL) { + const appUrl = baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + + // Go directly to login page first + await page.goto(appUrl + '/login'); + + // Fill credentials and login + const email = process.env.E2E_EMAIL || 'test@tududi.com'; + const password = process.env.E2E_PASSWORD || 'password123'; + + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: /login/i }).click(); + + // Wait for redirect to Today view + await expect(page).toHaveURL(/\/today$/); + + // Navigate to tags page + await page.goto(appUrl + '/tags'); + await expect(page).toHaveURL(/\/tags/); + + return appUrl; +} + +// Shared function to create a tag via the sidebar button +async function createTag(page, tagName) { + // Find the "Add Tag" button in the sidebar + const addTagButton = page.locator('button[aria-label*="Tag"]'); + await expect(addTagButton).toBeVisible(); + + // Click the Add Tag button + await addTagButton.click(); + + // Wait for the Tag Modal to appear + await expect(page.locator('input[name="name"]')).toBeVisible({ timeout: 10000 }); + + // Fill in the tag name + await page.locator('input[name="name"]').fill(tagName); + + // Save the tag + await page.getByRole('button', { name: /create.*tag|save/i }).click(); + + // Wait for the modal to close + await expect(page.locator('input[name="name"]')).not.toBeVisible({ timeout: 10000 }); + + // Wait for tag creation to complete + await page.waitForTimeout(2000); +} + +test('user can create a new tag and verify it appears in the tags list', async ({ page, baseURL }) => { + await loginAndNavigateToTags(page, baseURL); + + // Create a unique test tag + const timestamp = Date.now(); + const tagName = `TestTag${timestamp}`; + await createTag(page, tagName); + + // Verify the tag appears in the tags list + await expect(page.getByText(tagName)).toBeVisible({ timeout: 10000 }); +}); + +test('user can update an existing tag', async ({ page, baseURL }) => { + await loginAndNavigateToTags(page, baseURL); + + // Create an initial tag + const timestamp = Date.now(); + const originalTagName = `TestTagEdit${timestamp}`; + await createTag(page, originalTagName); + + // Find the tag container and hover to show edit button + const tagContainer = page.getByText(originalTagName).locator('..'); + await tagContainer.hover(); + + // Click the edit button (pencil icon) + await tagContainer.locator('button[aria-label*="Edit"], button[title*="Edit"]').click(); + + // Wait for the Tag Modal to appear with the tag data + await expect(page.locator('input[name="name"]')).toBeVisible(); + + // Verify the tag name field is pre-filled + const tagNameInput = page.locator('input[name="name"]').first(); + await expect(tagNameInput).toHaveValue(originalTagName); + + // Edit the tag name + const editedTagName = `EditedTestTag${timestamp}`; + await tagNameInput.clear(); + await tagNameInput.fill(editedTagName); + + // Save the changes + await page.getByRole('button', { name: /save|update/i }).click(); + + // Wait for the modal to close + await expect(page.locator('input[name="name"]')).not.toBeVisible(); + + // Verify the edited tag appears in the tags list + await expect(page.getByText(editedTagName)).toBeVisible(); + + // Verify the original tag name is no longer visible + await expect(page.getByText(originalTagName)).not.toBeVisible(); +}); + +test('user can delete an existing tag', async ({ page, baseURL }) => { + await loginAndNavigateToTags(page, baseURL); + + // Create an initial tag + const timestamp = Date.now(); + const tagName = `TestTagDelete${timestamp}`; + await createTag(page, tagName); + + // Find the tag container and hover to show delete button + const tagContainer = page.getByText(tagName).locator('..'); + await tagContainer.hover(); + + // Click the delete button (trash icon) + await tagContainer.locator('button[aria-label*="Delete"], button[title*="Delete"]').click(); + + // Wait for and handle the confirmation dialog + await expect(page.locator('text=Delete Tag')).toBeVisible(); + // Click the confirm button in the confirmation dialog + await page.getByRole('button', { name: /confirm|delete/i }).click(); + + // Verify the tag is no longer visible in the tags list + await expect(page.getByText(tagName)).not.toBeVisible(); +}); \ No newline at end of file diff --git a/e2e/tests/task.spec.ts b/e2e/tests/task.spec.ts new file mode 100644 index 0000000..462d2e7 --- /dev/null +++ b/e2e/tests/task.spec.ts @@ -0,0 +1,144 @@ +import { test, expect } from '@playwright/test'; + +// Shared login function +async function loginAndNavigateToTasks(page, baseURL) { + const appUrl = baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + + // Go directly to login page first + await page.goto(appUrl + '/login'); + + // Fill credentials and login + const email = process.env.E2E_EMAIL || 'test@tududi.com'; + const password = process.env.E2E_PASSWORD || 'password123'; + + await page.getByLabel('Email').fill(email); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: /login/i }).click(); + + // Wait for redirect to Today view + await expect(page).toHaveURL(/\/today$/); + + // Navigate to tasks page + await page.goto(appUrl + '/tasks'); + await expect(page).toHaveURL(/\/tasks/); + + return appUrl; +} + +// Shared function to create a task via the inline input field +async function createTask(page, taskName) { + // Find task input - try multiple selectors to be more robust + let taskInput; + + // Find the NewTask input field more specifically to avoid the search field + // Look for input with the exact placeholder text from NewTask component + try { + taskInput = page.locator('input[placeholder="Προσθήκη Νέας Εργασίας"]').first(); + await expect(taskInput).toBeVisible({ timeout: 5000 }); + } catch { + // Fallback: look for input within the NewTask component structure + // NewTask has a container with rounded-lg shadow-sm and a PlusCircleIcon + taskInput = page.locator('.rounded-lg.shadow-sm').filter({ has: page.locator('svg') }).locator('input[type="text"]').first(); + await expect(taskInput).toBeVisible({ timeout: 5000 }); + } + + // Clear and fill in the task name + await taskInput.clear(); + await taskInput.fill(taskName); + + // Press Enter to create the task + await taskInput.press('Enter'); + + // Verify task creation by checking that the input field is cleared + // (this is simpler and more reliable than trying to find the created task in the UI) + await expect(taskInput).toHaveValue(''); + + // Wait for the task creation API call to complete + await page.waitForTimeout(2000); +} + +test('user can create a new task and verify it appears in the task list', async ({ page, baseURL }) => { + await loginAndNavigateToTasks(page, baseURL); + + // Create a unique test task + const timestamp = Date.now(); + const taskName = `Test task ${timestamp}`; + await createTask(page, taskName); +}); + +test('user can update an existing task', async ({ page, baseURL }) => { + await loginAndNavigateToTasks(page, baseURL); + + // Create an initial task + const timestamp = Date.now(); + const originalTaskName = `Test task to edit ${timestamp}`; + await createTask(page, originalTaskName); + + // Find the task and click on it to open the edit modal + const taskContainer = page.locator('.task-item-wrapper').filter({ hasText: originalTaskName }); + await taskContainer.click(); + + // Wait for the Task Modal to appear with the task data + await expect(page.locator('input[name="name"], input[placeholder*="task" i], input[placeholder*="name" i]')).toBeVisible(); + + // Verify the task name field is pre-filled + const taskNameInput = page.locator('input[name="name"], input[placeholder*="task" i], input[placeholder*="name" i]').first(); + await expect(taskNameInput).toHaveValue(originalTaskName); + + // Edit the task name + const editedTaskName = `Edited test task ${timestamp}`; + await taskNameInput.clear(); + await taskNameInput.fill(editedTaskName); + + // Save the changes + await page.locator('.bg-blue-600.text-white').filter({ hasText: 'Save' }).click(); + + // Wait for the modal to close + await expect(page.locator('input[name="name"], input[placeholder*="task" i], input[placeholder*="name" i]')).not.toBeVisible(); + + // Verify the edited task appears in the task list + await expect(page.locator('.task-item-wrapper').filter({ hasText: editedTaskName })).toBeVisible(); + + // Verify the original task name is no longer visible + await expect(page.locator('.task-item-wrapper').filter({ hasText: originalTaskName })).not.toBeVisible(); +}); + +test('user can delete an existing task', async ({ page, baseURL }) => { + await loginAndNavigateToTasks(page, baseURL); + + // Create an initial task + const timestamp = Date.now(); + const taskName = `Test task to delete ${timestamp}`; + await createTask(page, taskName); + + // Find the task container and hover to show action buttons + const taskContainer = page.locator('.task-item-wrapper').filter({ hasText: taskName }); + await taskContainer.hover(); + + // Click the delete button (trash icon) + await taskContainer.locator('button[title="Delete"], button').filter({ hasText: '' }).last().click(); + + // Wait for and handle the confirmation dialog + await expect(page.locator('text=Delete Task')).toBeVisible(); + // Click the red "Delete" button in the confirmation dialog + await page.locator('.bg-red-500.text-white').click(); + + // Verify the task is no longer visible in the task list + await expect(page.locator('.task-item-wrapper').filter({ hasText: taskName })).not.toBeVisible(); +}); + +test('user can mark a task as complete', async ({ page, baseURL }) => { + await loginAndNavigateToTasks(page, baseURL); + + // Create an initial task + const timestamp = Date.now(); + const taskName = `Test task to complete ${timestamp}`; + await createTask(page, taskName); + + // Find the task container and click the checkbox to mark it as complete + const taskContainer = page.locator('.task-item-wrapper').filter({ hasText: taskName }); + await taskContainer.locator('input[type="checkbox"], button[role="checkbox"]').click(); + + // Verify the task is marked as completed (usually with strikethrough or different styling) + await expect(taskContainer.locator('.line-through, .completed, .opacity-50')).toBeVisible(); +}); \ No newline at end of file