From b46e242cb73a9cc3391fb0c1ad195e8b58ff4045 Mon Sep 17 00:00:00 2001 From: zhi Date: Mon, 16 Mar 2026 07:26:38 +0000 Subject: [PATCH] feat: add task.spec.ts (task + issue comment flow), keep milestone data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - task.spec.ts: login → create project → create milestone → create task → create issue → add comment → verify → logout (no cleanup) - milestone.spec.ts: remove cleanup step, keep project/milestone for inspection --- tests/milestone.spec.ts | 25 +----- tests/task.spec.ts | 171 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 24 deletions(-) create mode 100644 tests/task.spec.ts 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!'); + }); +});