dev1/tests/e2e/49-career-coach-quick-actions.spec.mjs

158 lines
7.2 KiB
JavaScript

// @ts-check
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
const j = (o) => JSON.stringify(o);
test.describe('@p1 CareerCoach — Quick Actions (49)', () => {
test.setTimeout(20000);
test('Networking Plan + Interview Help show coach replies', async ({ page, request }) => {
const u = loadTestUser();
// ── Premium/user-profile gate (include area/state so downstream fetches are happy)
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',
career_situation: 'planning',
key_skills: 'javascript, communication'
})
})
);
// ── 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 a scenario profile server-side
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 CareerRoadmap deps
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',
status: 'planned',
career_goals: '1. Improve networking\n2. Prepare for interviews',
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' }) })
);
// Salary/projections to keep UI calm (not central to this test)
await page.route(/\/api\/salary\?*/i, r => r.fulfill({
status: 200, contentType: 'application/json',
body: j({ regional: null, national: null })
}));
await page.route(/\/api\/projections\/15-1252\?state=.*/i, r => r.fulfill({
status: 200, contentType: 'application/json', body: j({})
}));
// Milestones list empty (not the focus)
await page.route(new RegExp(`/api/premium/milestones\\?careerProfileId=${career_profile_id}$`, 'i'), r =>
r.fulfill({ status: 200, contentType: 'application/json', body: j({ milestones: [] }) })
);
// ── Coach thread plumbing
let coachThreadId = 't-123';
await page.route(/\/api\/premium\/coach\/chat\/threads$/i, async r => {
if (r.request().method() === 'GET') {
return r.fulfill({ status: 200, contentType: 'application/json', body: j({ threads: [] }) });
}
if (r.request().method() === 'POST') {
return r.fulfill({ status: 200, contentType: 'application/json', body: j({ id: coachThreadId }) });
}
return r.fallback();
});
await page.route(new RegExp(`/api/premium/coach/chat/threads/${coachThreadId}$`, 'i'), r =>
r.fulfill({ status: 200, contentType: 'application/json', body: j({ messages: [] }) })
);
// POST messages — simulate small OpenAI delay, return deterministic reply
await page.route(new RegExp(`/api/premium/coach/chat/threads/${coachThreadId}/messages$`, 'i'), async r => {
// tiny delay to surface “Coach is typing…”
await new Promise(res => setTimeout(res, 350));
let body = {};
try { body = await r.request().postDataJSON(); } catch {}
// Always return a deterministic but generic reply (we won't assert exact text)
return r.fulfill({
status: 200, contentType: 'application/json',
body: j({ reply: 'Coach reply.' })
});
});
// ── Go to Roadmap
await page.goto(`/career-roadmap/${career_profile_id}`, { waitUntil: 'domcontentloaded' });
// Quick-action buttons visible
await expect(page.getByRole('button', { name: 'Networking Plan' })).toBeVisible({ timeout: 6000 });
await expect(page.getByRole('button', { name: 'Job-Search Plan' })).toBeVisible({ timeout: 6000 });
await expect(page.getByRole('button', { name: 'Interview Help' })).toBeVisible({ timeout: 6000 });
await expect(page.getByRole('button', { name: /Grow Career with AI/i })).toBeVisible({ timeout: 6000 });
await expect(page.getByRole('button', { name: /Edit Goals/i })).toBeVisible({ timeout: 6000 });
// ===== Networking Plan flow =====
// Count assistant bubbles before (assistant bubbles use the gray bg class)
const coachArea = page.locator('div.overflow-y-auto.border.rounded');
const assistantBubbles = coachArea.locator('div.bg-gray-200');
const beforeCount = await assistantBubbles.count();
await page.getByRole('button', { name: 'Networking Plan' }).click();
// Note message appears immediately (from component)
await expect(page.getByText(/create a Networking roadmap/i)).toBeVisible({ timeout: 4000 });
// “Coach is typing…” shows then disappears after reply
await expect(page.getByText('Coach is typing…')).toBeVisible({ timeout: 1200 });
await expect(page.getByText('Coach is typing…')).toHaveCount(0, { timeout: 6000 });
// Assistant bubbles increased by 1 after reply
await expect(async () => {
const after = await assistantBubbles.count();
expect(after).toBeGreaterThan(beforeCount);
}).toPass({ timeout: 6000 });
// ===== Interview Help flow =====
const beforeInterview = await assistantBubbles.count();
await page.getByRole('button', { name: 'Interview Help' }).click();
// Intro note
await expect(page.getByText(/Starting mock interview/i)).toBeVisible({ timeout: 4000 });
// Typing indicator
await expect(page.getByText('Coach is typing…')).toBeVisible({ timeout: 1200 });
await expect(page.getByText('Coach is typing…')).toHaveCount(0, { timeout: 6000 });
// Assistant bubbles increased again after the interview reply
await expect(async () => {
const after = await assistantBubbles.count();
expect(after).toBeGreaterThan(beforeInterview);
}).toPass({ timeout: 6000 });
});
});