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 task item -> 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.task-title:has-text("${taskTitle}")`); await expect(taskRow).toBeVisible({ timeout: 10000 }); console.log('✅ Task created and verified'); // ── Step 5: Create a Task item (for comment testing) ─────────────── // Milestone tasks don't have a detail page with comments, so we create // a Task item via the /tasks/new form to test the comment flow. console.log('📋 Creating task item for comment testing...'); await page.goto(`${BASE_URL}/tasks/new`); await page.waitForLoadState('networkidle'); const taskItemTitle = `Test Task for Comment ${TS}`; await page.getByTestId('task-title-input').fill(taskItemTitle); await page.getByTestId('task-description-input').fill('Task created for comment testing'); // Select our project and wait for milestones to load await page.getByTestId('project-select').selectOption({ label: projectName }); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); const milestoneSelect = page.getByTestId('milestone-select'); await expect.poll(async () => { const options = await milestoneSelect.locator('option').allTextContents(); return options.includes(milestoneName); }, { timeout: 10000 }).toBeTruthy(); const msOptions = await milestoneSelect.locator('option').allTextContents(); console.log('Milestone options:', msOptions); await milestoneSelect.selectOption({ label: milestoneName }); await page.getByTestId('task-type-select').selectOption('task'); await page.waitForTimeout(300); const createTaskResponsePromise = page.waitForResponse((response) => { return response.request().method() === 'POST' && /\/tasks(?:\?|$)/.test(response.url()); }); await page.getByTestId('create-task-button').click(); const createTaskResponse = await createTaskResponsePromise; expect(createTaskResponse.ok()).toBeTruthy(); const createdTask = await createTaskResponse.json(); console.log('Created task item response:', createdTask); await page.waitForURL(`${BASE_URL}/tasks`, { timeout: 10000 }); await page.waitForLoadState('networkidle'); console.log('✅ Task item created'); // ── Step 6: Open Task → Add Comment ──────────────────────────────── console.log('💬 Opening task to add comment...'); await page.goto(`${BASE_URL}/tasks/${createdTask.id}`); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); // Verify we're on task detail await page.waitForSelector(`h2:has-text("${taskItemTitle}")`, { 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!'); }); });