/** * Propose E2E Tests * Covers: create, list, view, edit, accept, reject, reopen flows */ 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('Propose Management', () => { const login = async (page: any) => { 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: any) => 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(); console.log('✅ Logged in'); }; const createProject = async (page: any, name: string) => { console.log(`📁 Creating project: ${name}`); await page.goto(`${BASE_URL}/projects`); await page.waitForLoadState('networkidle'); await page.click('button:has-text("+ New")'); await page.waitForSelector('.modal', { timeout: 10000 }); await page.fill('.modal input[placeholder*="Project name"]', name); await page.fill('.modal textarea', 'Test project description'); await page.click('.modal button:has-text("Create")'); await page.waitForTimeout(1000); console.log('✅ Project created'); }; const createMilestone = async (page: any, projectName: string, name: string) => { console.log(`📌 Creating milestone: ${name}`); await page.goto(`${BASE_URL}/projects`); await page.waitForLoadState('networkidle'); await page.click(`.project-card:has-text("${projectName}")`); await page.waitForURL(/\/projects\/\d+/, { timeout: 10000 }); await page.click('button:has-text("+ New")'); await page.waitForSelector('.modal', { timeout: 10000 }); await page.fill('input[data-testid="milestone-title-input"]', name); await page.click('.modal button:has-text("Create")'); await page.waitForTimeout(1000); console.log('✅ Milestone created'); }; test.describe('Propose CRUD Flow', () => { test('create propose -> view -> edit -> list', async ({ page }) => { const TS = Date.now(); await login(page); const projectName = `Propose Test Project ${TS}`; await createProject(page, projectName); // Navigate to Proposes page await page.goto(`${BASE_URL}/proposes`); await page.waitForLoadState('networkidle'); // Select project filter await page.selectOption('select', { label: projectName }); await page.waitForTimeout(500); // Create propose const proposeTitle = `Test Propose ${TS}`; const proposeDesc = `Description for propose ${TS}`; await page.click('button:has-text("+ New Propose")'); await page.waitForSelector('.modal', { timeout: 10000 }); await page.fill('.modal input[placeholder*="Propose title"]', proposeTitle); await page.fill('.modal textarea', proposeDesc); await page.click('.modal button:has-text("Create")'); // Wait for list to update await page.waitForTimeout(1000); // Verify in list await expect(page.locator(`.milestone-card:has-text("${proposeTitle}")`)).toBeVisible({ timeout: 10000 }); // View propose detail await page.click(`.milestone-card:has-text("${proposeTitle}")`); await page.waitForURL(/\/proposes\/\d+/, { timeout: 10000 }); // Verify detail page await expect(page.locator('h2')).toContainText(proposeTitle); await expect(page.locator('.section').filter({ has: page.locator('h3:has-text("Description")') }).locator('p')).toContainText(proposeDesc); await expect(page.locator('.badge:has-text("open")')).toBeVisible(); // Edit propose await page.click('button:has-text("Edit")'); await page.waitForSelector('.modal', { timeout: 10000 }); const newTitle = `${proposeTitle} (edited)`; await page.fill('.modal input', newTitle); await page.click('.modal button:has-text("Save")'); await page.waitForTimeout(500); // Verify edit await expect(page.locator('h2')).toContainText(newTitle); console.log('✅ Propose CRUD test passed'); }); }); test.describe('Propose Lifecycle Flow', () => { test('accept propose -> creates feature story task', async ({ page }) => { const TS = Date.now(); await login(page); const projectName = `Accept Propose Project ${TS}`; await createProject(page, projectName); // Create milestone for accept await createMilestone(page, projectName, `Milestone for Accept ${TS}`); // Create propose await page.goto(`${BASE_URL}/proposes`); await page.selectOption('select', { label: projectName }); await page.waitForTimeout(500); const proposeTitle = `Accept Test Propose ${TS}`; await page.click('button:has-text("+ New Propose")'); await page.fill('.modal input', proposeTitle); await page.fill('.modal textarea', 'To be accepted'); await page.click('.modal button:has-text("Create")'); await page.waitForTimeout(1000); // Open propose detail await page.click(`.milestone-card:has-text("${proposeTitle}")`); await page.waitForURL(/\/proposes\/\d+/, { timeout: 10000 }); // Accept propose await page.click('button:has-text("Accept")'); await page.waitForSelector('.modal:has-text("Accept Propose")', { timeout: 10000 }); // Select milestone await page.selectOption('.modal select', { label: `Milestone for Accept ${TS}` }); await page.click('.modal button:has-text("Confirm Accept")'); await page.waitForTimeout(1000); // Verify status changed await expect(page.locator('.badge:has-text("accepted")')).toBeVisible({ timeout: 10000 }); await expect(page.locator('button:has-text("View Generated Task")')).toBeVisible(); console.log('✅ Propose accept test passed'); }); test('reject propose -> can reopen', async ({ page }) => { const TS = Date.now(); await login(page); const projectName = `Reject Propose Project ${TS}`; await createProject(page, projectName); // Create propose await page.goto(`${BASE_URL}/proposes`); await page.selectOption('select', { label: projectName }); await page.waitForTimeout(500); const proposeTitle = `Reject Test Propose ${TS}`; await page.click('button:has-text("+ New Propose")'); await page.fill('.modal input', proposeTitle); await page.click('.modal button:has-text("Create")'); await page.waitForTimeout(1000); // Open propose detail await page.click(`.milestone-card:has-text("${proposeTitle}")`); await page.waitForURL(/\/proposes\/\d+/, { timeout: 10000 }); // Reject propose (current UI uses prompt, not modal) page.once('dialog', async (dialog) => { expect(dialog.type()).toBe('prompt'); await dialog.accept('Rejected by automated test'); }); await page.click('button:has-text("Reject")'); await page.waitForTimeout(1000); // Verify status changed await expect(page.locator('.badge:has-text("rejected")')).toBeVisible({ timeout: 10000 }); // Reopen propose (current UI is direct action, no modal) await page.click('button:has-text("Reopen")'); await page.waitForTimeout(1000); // Verify back to open await expect(page.locator('.badge:has-text("open")')).toBeVisible({ timeout: 10000 }); console.log('✅ Propose reject/reopen test passed'); }); test('edit button hidden for accepted propose', async ({ page }) => { const TS = Date.now(); await login(page); const projectName = `Edit Hidden Project ${TS}`; await createProject(page, projectName); await createMilestone(page, projectName, `Milestone ${TS}`); // Create and accept propose await page.goto(`${BASE_URL}/proposes`); await page.selectOption('select', { label: projectName }); await page.waitForTimeout(500); await page.click('button:has-text("+ New Propose")'); await page.fill('.modal input', `Accept Test ${TS}`); await page.click('.modal button:has-text("Create")'); await page.waitForTimeout(1000); await page.click(`.milestone-card:has-text("Accept Test")`); await page.waitForURL(/\/proposes\/\d+/, { timeout: 10000 }); // Accept await page.click('button:has-text("Accept")'); await page.waitForSelector('.modal:has-text("Accept Propose")', { timeout: 10000 }); await page.selectOption('.modal select', { label: `Milestone ${TS}` }); await page.click('.modal button:has-text("Confirm Accept")'); await page.waitForTimeout(1000); // Verify Edit button is hidden await expect(page.locator('button:has-text("Edit")')).not.toBeVisible(); await expect(page.locator('.badge:has-text("accepted")')).toBeVisible(); console.log('✅ Edit button hidden test passed'); }); }); test.describe('Propose Permissions', () => { test('cannot create propose without project selection', async ({ page }) => { await login(page); await page.goto(`${BASE_URL}/proposes`); await page.waitForLoadState('networkidle'); // Button should be disabled const button = page.locator('button:has-text("+ New Propose")'); await expect(button).toBeDisabled(); console.log('✅ Propose create disabled test passed'); }); }); });