diff --git a/tests/milestone.spec.ts b/tests/milestone.spec.ts index 59f04ac..5cd8ee0 100644 --- a/tests/milestone.spec.ts +++ b/tests/milestone.spec.ts @@ -135,30 +135,7 @@ test.describe('Milestone Editor', () => { expect(hasMilestone).toBe(true); console.log('โœ… Milestone verified in UI'); - // Step 6: Cleanup โ€” delete the project we created - console.log('๐Ÿงน Cleaning up โ€” deleting project...'); - await page.click('a:has-text("๐Ÿ“ Projects")'); - await page.waitForLoadState('networkidle'); - await page.waitForSelector(`.project-card:has-text("${projectName}")`, { timeout: 10000 }); - await page.click(`.project-card:has-text("${projectName}")`); - await page.waitForLoadState('networkidle'); - await page.waitForTimeout(1000); - - const deleteBtn = page.locator('button:has-text("Delete")'); - if (await deleteBtn.isVisible().catch(() => false)) { - page.on('dialog', async dialog => { - const match = dialog.message().match(/Type the project name "(.*)" to confirm/); - if (match) { - await dialog.accept(match[1]); - } else { - await dialog.accept(); - } - }); - await deleteBtn.click(); - await page.waitForTimeout(2000); - console.log('โœ… Project cleaned up'); - } - + // Note: intentionally NOT deleting the project/milestone โ€” leave for inspection console.log('๐ŸŽ‰ All milestone tests passed!'); }); }); diff --git a/tests/task.spec.ts b/tests/task.spec.ts new file mode 100644 index 0000000..ca5fa97 --- /dev/null +++ b/tests/task.spec.ts @@ -0,0 +1,171 @@ +import { test, expect } from '@playwright/test'; + +const BASE_URL = process.env.BASE_URL || process.env.FRONTEND_URL || 'http://frontend:3000'; + +const ADMIN_USERNAME = 'admin'; +const ADMIN_PASSWORD = 'admin123'; + +test.describe('Task & Comment Flow', () => { + test('login -> create project -> create milestone -> create task -> create issue -> comment -> logout', async ({ page }) => { + const TS = Date.now(); + + // โ”€โ”€ Step 1: Login โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('๐Ÿ” Logging in...'); + const MAX_LOGIN_RETRIES = 3; + for (let attempt = 1; attempt <= MAX_LOGIN_RETRIES; attempt++) { + console.log(`Login attempt ${attempt}/${MAX_LOGIN_RETRIES}`); + await page.goto(`${BASE_URL}/login`); + await page.waitForLoadState('networkidle'); + + await page.fill('input[type="text"], input[name="username"]', ADMIN_USERNAME); + await page.fill('input[type="password"], input[name="password"]', ADMIN_PASSWORD); + + const loginPromise = page.waitForResponse( + (r) => r.url().includes('/auth/token') && r.request().method() === 'POST', + { timeout: 15000 } + ).catch(() => null); + + await page.click('button[type="submit"], button:has-text("Sign in")'); + + const loginResp = await loginPromise; + if (loginResp && loginResp.status() === 200) { + await page.waitForURL(`${BASE_URL}/**`, { timeout: 10000 }).catch(() => {}); + await page.waitForLoadState('networkidle'); + break; + } + if (attempt < MAX_LOGIN_RETRIES) { + console.log('Retrying login in 3s...'); + await page.waitForTimeout(3000); + } + } + + const token = await page.evaluate(() => localStorage.getItem('token')); + expect(token, 'Login failed').toBeTruthy(); + await page.waitForSelector('a:has-text("๐Ÿ“ Projects")', { timeout: 10000 }); + console.log('โœ… Logged in'); + + // โ”€โ”€ Step 2: Create Project โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const projectName = `Task Test Project ${TS}`; + console.log(`๐Ÿ“ Creating project: ${projectName}`); + await page.click('a:has-text("๐Ÿ“ Projects")'); + await page.waitForLoadState('networkidle'); + + await page.waitForSelector('button:has-text("+ New")', { timeout: 10000 }); + await page.click('button:has-text("+ New")'); + await page.waitForSelector('input[placeholder="Project name"]', { timeout: 10000 }); + await page.fill('input[placeholder="Project name"]', projectName); + await page.fill('input[placeholder="Description (optional)"]', 'Project for task & comment testing'); + await page.click('button:has-text("Create")'); + await page.waitForTimeout(2000); + + await page.waitForSelector(`.project-card:has-text("${projectName}")`, { timeout: 10000 }); + console.log('โœ… Project created'); + + // โ”€โ”€ Step 3: Open project โ†’ Create Milestone โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('๐Ÿ“Œ Opening project & creating milestone...'); + await page.click(`.project-card:has-text("${projectName}")`); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1500); + + // Scroll to milestone section and click "+ New" + await page.evaluate(() => window.scrollTo(0, 500)); + await page.waitForTimeout(500); + await page.click('button:has-text("+ New")'); + await page.waitForSelector('.modal', { timeout: 5000 }); + + const milestoneName = `Task Test Milestone ${TS}`; + console.log(`Milestone name: ${milestoneName}`); + await page.fill('input[placeholder="Milestone title"]', milestoneName); + await page.waitForTimeout(500); + await page.locator('.modal button.btn-primary').click(); + await page.waitForTimeout(2000); + await page.waitForSelector('.modal', { state: 'detached', timeout: 5000 }).catch(() => {}); + + // Verify milestone created + const headingText = await page.locator('h3').filter({ hasText: /Milestones/i }).textContent(); + expect(headingText).not.toContain('(0)'); + console.log('โœ… Milestone created'); + + // โ”€โ”€ Step 4: Enter Milestone Detail โ†’ Create Task โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('๐Ÿ“ Entering milestone detail...'); + await page.click(`.milestone-item:has-text("${milestoneName}")`); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(2000); + + // Wait for milestone detail page + await page.waitForSelector('h2:has-text("๐Ÿ")', { timeout: 10000 }); + console.log('Milestone detail loaded'); + + // Click "+ Create Task" + await page.click('button:has-text("+ Create Task")'); + await page.waitForTimeout(500); + + const taskTitle = `Test Task ${TS}`; + console.log(`Task title: ${taskTitle}`); + await page.fill('input[placeholder="Title"]', taskTitle); + await page.fill('textarea[placeholder="Description (optional)"]', 'Task created by automated test'); + await page.waitForTimeout(300); + + // Click Create button in the card form + await page.click('.card button.btn-primary'); + await page.waitForTimeout(2000); + + // Verify task appears in the Tasks tab + await page.click('.tab:has-text("Tasks")'); + await page.waitForTimeout(1000); + const taskRow = page.locator(`td.issue-title:has-text("${taskTitle}")`); + await expect(taskRow).toBeVisible({ timeout: 10000 }); + console.log('โœ… Task created and verified'); + + // โ”€โ”€ Step 5: Create Issue (for comment testing) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Tasks don't have a detail page with comments, so we create an Issue + // of type "task" to test the comment flow. + console.log('๐Ÿ“‹ Creating issue for comment testing...'); + await page.goto(`${BASE_URL}/issues/new`); + await page.waitForLoadState('networkidle'); + + const issueTitle = `Test Issue for Comment ${TS}`; + await page.fill('label:has-text("Title") input', issueTitle); + await page.fill('label:has-text("Description") textarea', 'Issue created for comment testing'); + + // Select our project + await page.selectOption('label:has-text("Projects") select', { label: projectName }); + // Select type "Task" + await page.selectOption('label:has-text("Type") select', 'task'); + await page.waitForTimeout(300); + + await page.click('button.btn-primary:has-text("Create")'); + await page.waitForURL(`${BASE_URL}/issues`, { timeout: 10000 }); + await page.waitForLoadState('networkidle'); + console.log('โœ… Issue created'); + + // โ”€โ”€ Step 6: Open Issue โ†’ Add Comment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('๐Ÿ’ฌ Opening issue to add comment...'); + await page.click(`text=${issueTitle}`); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + + // Verify we're on issue detail + await page.waitForSelector(`h2:has-text("${issueTitle}")`, { timeout: 10000 }); + + const commentText = `Automated test comment ${TS}`; + console.log(`Comment: ${commentText}`); + await page.fill('textarea[placeholder="Add a comment..."]', commentText); + await page.click('button:has-text("Submit comment")'); + await page.waitForTimeout(2000); + + // Verify comment appears + const commentEl = page.locator(`.comment p:has-text("${commentText}")`); + await expect(commentEl).toBeVisible({ timeout: 10000 }); + console.log('โœ… Comment added and verified'); + + // โ”€โ”€ Step 7: Logout (do NOT delete the project) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('๐Ÿšช Logging out...'); + await page.click('button:has-text("Log out")'); + await page.waitForFunction(() => !localStorage.getItem('token'), { timeout: 10000 }); + await page.waitForSelector('button:has-text("Log in")', { timeout: 10000 }); + console.log('โœ… Logged out'); + + console.log('๐ŸŽ‰ All task & comment tests passed!'); + }); +});