// @ts-nocheck import { test, expect } from '@playwright/test'; import { loadTestUser } from '../utils/testUser.js'; const j = (o) => JSON.stringify(o); /** Safe helpers for @ts-check */ /** @param {string} url */ const lastNum = (url) => { const m = url.match(/(\d+)$/); return m ? Number(m[1]) : 0; }; /** @param {string} url @param {string} key */ const getNumParam = (url, key) => { try { const u = new URL(url); const v = u.searchParams.get(key); return Number(v || '0'); } catch { return 0; } }; test.describe('@p1 Milestones — CRUD + Drawer (48)', () => { test.setTimeout(22000); test('Create, edit, toggle task in drawer, and delete milestone', async ({ page, request }) => { const u = loadTestUser(); // ── Premium gate await page.route( /\/api\/user-profile\?fields=.*(firstname|is_premium|is_pro_premium|area|state).*/i, r => r.fulfill({ status: 200, contentType: 'application/json', body: j({ firstname: 'Tester', is_premium: 1, is_pro_premium: 0, area: 'Atlanta-Sandy Springs-Roswell, GA', state: 'GA' }) }) ); // ── Sign in (no storage-state bypass) await page.context().clearCookies(); await page.goto('/signin', { waitUntil: 'domcontentloaded' }); await page.getByPlaceholder('Username', { exact: true }).fill(u.username); await page.getByPlaceholder('Password', { exact: true }).fill(u.password); await page.getByRole('button', { name: /^Sign In$/ }).click(); await page.waitForURL('**/signin-landing**', { timeout: 15000 }); // ── Seed scenario const scen = await request.post('/api/premium/career-profile', { data: { career_name: 'Software Developers', status: 'planned', start_date: '2025-01-01' } }); const { career_profile_id } = await scen.json(); // ── Minimal background routes for CareerRoadmap await page.route(new RegExp(`/api/premium/career-profile/${career_profile_id}$`, 'i'), r => r.fulfill({ status: 200, contentType: 'application/json', body: j({ id: career_profile_id, career_name: 'Software Developers', scenario_title: 'Software Developers', soc_code: '15-1252.00', start_date: '2025-01-01', college_enrollment_status: 'not_enrolled' }) }) ); await page.route(/\/api\/premium\/financial-profile$/i, r => r.fulfill({ status: 200, contentType: 'application/json', body: j({ current_salary: 90000, additional_income: 0, monthly_expenses: 2500, monthly_debt_payments: 300, retirement_savings: 10000, emergency_fund: 3000, retirement_contribution: 300, emergency_contribution: 200, extra_cash_emergency_pct: 50, extra_cash_retirement_pct: 50 }) })); await page.route(new RegExp(`/api/premium/college-profile\\?careerProfileId=${career_profile_id}$`, 'i'), r => r.fulfill({ status: 200, contentType: 'application/json', body: j({ college_enrollment_status: 'not_enrolled', existing_college_debt: 0, interest_rate: 5, loan_term: 10, loan_deferral_until_graduation: 0, academic_calendar: 'monthly', annual_financial_aid: 0, tuition: 0, extra_payment: 0, expected_salary: 95000 }) }) ); await page.route(/\/api\/salary\?*/i, r => r.fulfill({ status: 200, contentType: 'application/json', body: j({ regional: { regional_PCT10: 60000, regional_MEDIAN: 100000, regional_PCT90: 160000 }, national: { national_PCT10: 55000, national_MEDIAN: 98000, national_PCT90: 155000 } }) })); await page.route(/\/api\/projections\/15-1252\?state=.*/i, r => r.fulfill({ status: 200, contentType: 'application/json', body: j({ state: { area: 'Georgia', baseYear: 2022, projectedYear: 2032, base: 45000, projection: 52000, change: 7000, annualOpenings: 3800, occupationName: 'Software Developers' }, national: { area: 'United States', baseYear: 2022, projectedYear: 2032, base: 1630000, projection: 1810000, change: 180000, annualOpenings: 153000, occupationName: 'Software Developers' } }) })); // ── In-memory milestone state for routes let createdMilestoneId = 99901; const existingMilestoneId = 88801; const existingTaskId = 50001; /** @type {{id:number,title:string,description:string,date:string,progress:number,status:string}[]} */ let milestoneList = [ { id: existingMilestoneId, title: 'Promotion Planning', description: 'Prepare for next level', date: '2026-04-01', progress: 0, status: 'planned' } ]; /** @type {Record>} */ let tasksByMilestone = { [existingMilestoneId]: [ { id: existingTaskId, title: 'Draft self-review', description: 'Write draft', due_date: '2026-03-15', status: 'not_started' } ], [createdMilestoneId]: [] }; /** @type {Record>} */ let impactsByMilestone = { [existingMilestoneId]: [], [createdMilestoneId]: [] }; // GET milestones await page.route(new RegExp(`/api/premium/milestones\\?careerProfileId=${career_profile_id}$`, 'i'), r => r.fulfill({ status: 200, contentType: 'application/json', body: j({ milestones: milestoneList }) }) ); // GET impacts/tasks await page.route(/\/api\/premium\/milestone-impacts\?milestone_id=\d+$/i, r => { const mid = Number(new URL(r.request().url()).searchParams.get('milestone_id')); r.fulfill({ status: 200, contentType: 'application/json', body: j({ impacts: impactsByMilestone[mid] || [] }) }); }); await page.route(/\/api\/premium\/tasks\?milestone_id=\d+$/i, r => { const mid = Number(new URL(r.request().url()).searchParams.get('milestone_id')); r.fulfill({ status: 200, contentType: 'application/json', body: j({ tasks: tasksByMilestone[mid] || [] }) }); }); // CREATE milestone await page.route(/\/api\/premium\/milestone$/i, async r => { if (r.request().method() !== 'POST') return r.fallback(); const body = await r.request().postDataJSON(); milestoneList = milestoneList.concat([{ id: createdMilestoneId, title: body.title, description: body.description || '', date: body.date || '2026-05-01', progress: body.progress || 0, status: body.status || 'planned' }]); return r.fulfill({ status: 200, contentType: 'application/json', body: j({ id: createdMilestoneId }) }); }); // CREATE impact / task await page.route(/\/api\/premium\/milestone-impacts$/i, async r => { if (r.request().method() !== 'POST') return r.fallback(); const body = await r.request().postDataJSON(); const mid = Number(body.milestone_id); const newId = Math.floor(Math.random() * 100000) + 70000; impactsByMilestone[mid] = (impactsByMilestone[mid] || []).concat([{ id: newId, ...body }]); return r.fulfill({ status: 200, contentType: 'application/json', body: j({ id: newId }) }); }); await page.route(/\/api\/premium\/tasks$/i, async r => { if (r.request().method() !== 'POST') return r.fallback(); const body = await r.request().postDataJSON(); const mid = Number(body.milestone_id); const newId = Math.floor(Math.random() * 100000) + 80000; tasksByMilestone[mid] = (tasksByMilestone[mid] || []).concat([{ id: newId, ...body }]); return r.fulfill({ status: 200, contentType: 'application/json', body: j({ id: newId }) }); }); // UPDATE milestone await page.route(/\/api\/premium\/milestones\/\d+$/i, async r => { if (r.request().method() !== 'PUT') return r.fallback(); const id = lastNum(r.request().url()); const body = await r.request().postDataJSON(); milestoneList = milestoneList.map(m => (m.id === id ? { ...m, ...body, id } : m)); return r.fulfill({ status: 200, contentType: 'application/json', body: j({ id }) }); }); // UPDATE/DELETE impacts await page.route(/\/api\/premium\/milestone-impacts\/\d+$/i, async r => { const id = lastNum(r.request().url()); if (r.request().method() === 'PUT') { const body = await r.request().postDataJSON(); const mid = getNumParam(r.request().url(), 'milestone_id'); impactsByMilestone[mid] = (impactsByMilestone[mid] || []).map(i => (i.id === id ? { ...i, ...body, id } : i)); return r.fulfill({ status: 200, body: '{}' }); } if (r.request().method() === 'DELETE') { for (const mid of Object.keys(impactsByMilestone)) { impactsByMilestone[mid] = impactsByMilestone[mid].filter(i => i.id !== id); } return r.fulfill({ status: 200, body: '{}' }); } return r.fallback(); }); // UPDATE/DELETE tasks await page.route(/\/api\/premium\/tasks\/\d+$/i, async r => { const id = lastNum(r.request().url()); if (r.request().method() === 'PUT') { const body = await r.request().postDataJSON(); for (const mid of Object.keys(tasksByMilestone)) { tasksByMilestone[mid] = tasksByMilestone[mid].map(t => (t.id === id ? { ...t, ...body, id } : t)); } return r.fulfill({ status: 200, body: '{}' }); } if (r.request().method() === 'DELETE') { for (const mid of Object.keys(tasksByMilestone)) { tasksByMilestone[mid] = tasksByMilestone[mid].filter(t => t.id !== id); } return r.fulfill({ status: 200, body: '{}' }); } return r.fallback(); }); // DELETE milestone await page.route(/\/api\/premium\/milestones\/\d+$/i, async r => { if (r.request().method() !== 'DELETE') return r.fallback(); const id = lastNum(r.request().url()); milestoneList = milestoneList.filter(m => m.id !== id); delete impactsByMilestone[id]; delete tasksByMilestone[id]; return r.fulfill({ status: 200, body: '{}' }); }); // ── Navigate await page.goto(`/career-roadmap/${career_profile_id}`, { waitUntil: 'domcontentloaded' }); // Milestones section const msHeader = page.getByRole('heading', { name: /Milestones/i }).first(); await msHeader.scrollIntoViewIfNeeded(); await expect(msHeader).toBeVisible({ timeout: 6000 }); // ===== CREATE ===== await page.getByRole('button', { name: /^\+ Add Milestone$/ }).click(); const modalHeader = page.getByRole('heading', { name: /^Milestones$/i }).first(); await expect(modalHeader).toBeVisible({ timeout: 6000 }); const modalRoot = modalHeader.locator('xpath=ancestor::div[contains(@class,"max-w-3xl")]').first(); // Open Add new milestone via DOM (robust) const rootHandle = await modalRoot.elementHandle(); if (rootHandle) { await rootHandle.evaluate((root) => { const summaries = Array.from(root.querySelectorAll('summary')); const target = summaries.find(s => (s.textContent || '').trim().toLowerCase().startsWith('add new milestone')); if (target) (target).click(); }); } const newSection = modalRoot.locator('summary:has-text("Add new milestone") + div').first(); await expect(newSection.locator('label:has-text("Title")')).toBeVisible({ timeout: 8000 }); // Fill create fields await newSection.locator('label:has-text("Title")').locator('..').locator('input').first() .fill('Launch Portfolio Website'); const createDate = newSection.locator('label:has-text("Date")').locator('..').locator('input').first(); const createDateType = (await createDate.getAttribute('type')) || ''; if (createDateType.toLowerCase() === 'date') { await createDate.fill('2026-05-01'); } else { await createDate.fill(''); await createDate.type('05/01/2026', { delay: 10 }); } await newSection.locator('label:has-text("Description")').locator('..').locator('textarea').first() .fill('Ship v1 and announce.'); // Impact (salary) await newSection.getByRole('button', { name: /^\+ Add impact$/ }).click(); await newSection.locator('label:has-text("Type")').locator('..').locator('select').last().selectOption('salary'); await newSection.locator('label:has-text("Amount")').locator('..').locator('input[type="number"]').last().fill('12000'); await newSection.locator('label:has-text("Start")').locator('..').locator('input[type="date"]').last().fill('2026-05-01'); // One task await newSection.getByRole('button', { name: /^\+ Add task$/ }).click(); await newSection.locator('label:has-text("Title")').last().locator('..').locator('input').fill('Buy domain'); await newSection.locator('label:has-text("Description")').last().locator('..').locator('input').fill('Pick .com'); await newSection.locator('label:has-text("Due Date")').last().locator('..').locator('input[type="date"]').fill('2026-04-15'); await newSection.locator('label:has-text("Status")').last().locator('..').locator('select').selectOption('not_started'); // Save await modalRoot.getByRole('button', { name: /^Save milestone$/i }).click(); // Ensure listed in panel and visible — expand the exact
that owns the row const createdRowText = page.getByText('Launch Portfolio Website').first(); const createdDetails = createdRowText.locator('xpath=ancestor::details[1]'); await createdDetails.locator('summary').first().click().catch(() => {}); await expect(createdRowText).toBeVisible({ timeout: 8000 }); // ===== EDIT ===== // Select row (visible under May 2026) and click pencil to edit const row = page.getByText('Launch Portfolio Website').first().locator('xpath=ancestor::li[1]'); await expect(row).toBeVisible({ timeout: 4000 }); await row.getByRole('button', { name: 'Edit milestone' }).click(); const editModal = page.getByRole('heading', { name: /^Milestones$/i }) .first().locator('xpath=ancestor::div[contains(@class,"max-w-3xl")]').first(); // Title & Date at top of expanded block await editModal.locator('label:has-text("Title")').locator('..').locator('input').first() .fill('Launch Portfolio Website v2'); const editDate = editModal.locator('label:has-text("Date")').locator('..').locator('input').first(); const editType = (await editDate.getAttribute('type')) || ''; if (editType.toLowerCase() === 'date') { await editDate.fill('2026-06-01'); } else { await editDate.fill(''); await editDate.type('06/01/2026', { delay: 10 }); } // Add another impact (Monthly) await editModal.getByRole('button', { name: /\+ Add impact/ }).first().click(); const typeSelects = editModal.locator('label:has-text("Type")').locator('..').locator('select'); await typeSelects.last().selectOption('MONTHLY'); await editModal.locator('label:has-text("Amount")').locator('..').locator('input[type="number"]').last().fill('150'); await editModal.locator('label:has-text("Start")').locator('..').locator('input[type="date"]').last().fill('2026-06-01'); await editModal.locator('label:has-text("End")').locator('..').locator('input[type="date"]').last().fill('2026-12-01'); // Add another task await editModal.getByRole('button', { name: /\+ Add task/ }).first().click(); await editModal.locator('label:has-text("Title")').locator('..').locator('input').last() .fill('Publish announcement'); // Save edits await editModal.getByRole('button', { name: /^Save$/ }).click(); // Bring Milestones back into view (modal is closed now) const msHeader2 = page.getByRole('heading', { name: /Milestones/i }).first(); await msHeader2.scrollIntoViewIfNeeded(); await expect(msHeader2).toBeVisible({ timeout: 4000 }); // Expand the exact
that contains the edited row const editedRowText = page.getByText('Launch Portfolio Website v2').first(); const editedDetails = editedRowText.locator('xpath=ancestor::details[1]'); await editedDetails.locator('summary').first().click().catch(() => {}); const rowV2Panel = editedRowText.locator('xpath=ancestor::li[1]'); await expect(rowV2Panel).toBeVisible({ timeout: 6000 }); // ===== DRAWER: open (no task assertions here — drawer-only smoke) ===== await rowV2Panel.click(); // opens drawer const drawer = page.locator('.fixed.inset-y-0.right-0').first(); await expect(drawer).toBeVisible({ timeout: 6000 }); // Close drawer (back chevron = first button in header) await drawer.locator('button').first().click(); // ===== DELETE ===== await rowV2Panel.getByRole('button', { name: 'Edit milestone' }).click(); const delModal = page.getByRole('heading', { name: /^Milestones$/i }).first() .locator('xpath=ancestor::div[contains(@class,"max-w-3xl")]').first(); // Confirm delete page.once('dialog', d => d.accept().catch(() => {})); await delModal.getByRole('button', { name: /^Delete milestone$/ }).click(); await expect(page.getByText('Launch Portfolio Website v2')).toHaveCount(0, { timeout: 8000 }); }); });