Added Premium Tests - fixed CollegeProfileForm.js

This commit is contained in:
Josh 2025-09-11 10:56:03 +00:00
parent 6ca3f8b5be
commit 2d8b82aca5
89 changed files with 2556 additions and 594 deletions

View File

@ -1 +1 @@
24c4644c626acf48ddca3964105cd9bfa267d82a-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
720d57c0d6d787c629f53d47689088a50b9e9b5b-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,27 @@
import { defineConfig } from '@playwright/test';
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Limit Playwright to E2E specs only
testDir: '/home/jcoakley/aptiva-dev1-app/tests/e2e',
testMatch: /.*\.spec\.(?:mjs|js|ts)$/,
use: {
baseURL: process.env.PW_BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
retries: 1,
reporter: [['list'], ['html', { open: 'never' }]],
});
use: {
baseURL: process.env.PW_BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
retries: 0,
// Add 'blob' so Playwright persists failures for --last-failed
reporter: [['list'], ['html', { open: 'never' }], ['blob']],
// Make Edge the default (you asked earlier)
projects: [
{
name: 'edge',
use: { ...devices['Desktop Edge'] },
},
],
});

View File

@ -2,6 +2,8 @@ import React, { useEffect, useState, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import apiFetch from '../auth/apiFetch.js';
import moment from 'moment/moment.js';
import Modal from './ui/modal.js';
import FinancialAidWizard from './FinancialAidWizard.js';
const authFetch = apiFetch; // keep local name, new implementation
/** -----------------------------------------------------------
@ -62,9 +64,11 @@ export default function CollegeProfileForm() {
const [graduationTouched, setGraduationTouched] = useState(false);
const [programLengthTouched, setProgramLengthTouched] = useState(false);
const [selectedUnitId, setSelectedUnitId] = useState(null);
const [showAidWizard, setShowAidWizard] = useState(false);
const schoolPrevRef = useRef('');
const programPrevRef = useRef('');
const lastSchoolText = useRef('');
const canonicalSchoolName = useRef(''); // the exact, server-known school name for selectedUnitId
const [form, setForm] = useState({
@ -130,7 +134,10 @@ const onSchoolInput = async (e) => {
selected_program: value ? prev.selected_program : '', // clear if user erased school
program_type: ''
}));
lastSchoolText.current = value;
setSelectedUnitId(null);
canonicalSchoolName.current = '';
setSchoolValid(value.trim() === ''); // empty = neutral, any text = invalid until validated
lastSchoolText.current = value;
}
if (!value.trim()) { setSchoolSug([]); schoolPrevRef.current = ''; return; }
const it = e?.nativeEvent?.inputType; // 'insertReplacementText' on datalist pick
@ -199,8 +206,10 @@ const onProgramInput = async (e) => {
})();
}, [form.selected_school, form.selected_program]);
// When selected_school changes (after commit/blur), reset program suggestions/types
useEffect(() => {
// Only clear when the change came from the user's typing (onSchoolInput sets lastSchoolText)
const typed = (form.selected_school || '').trim() === (lastSchoolText.current || '').trim();
if (!typed) return; // programmatic changes (initial load, API normalization) → keep UNITID & types
setProgSug([]);
setTypes([]);
setSelectedUnitId(null);
@ -228,7 +237,8 @@ const onProgramInput = async (e) => {
const n = Number(normalized.tuition);
setAutoTuition(Number.isFinite(n) ? n : 0);
}
if (normalized.unit_id) setSelectedUnitId(normalized.unit_id);
if (normalized.unit_id) setSelectedUnitId(normalized.unit_id);
if (normalized.selected_school) canonicalSchoolName.current = normalized.selected_school;
// If profile came with school+program, load types so Degree Type select is populated
if ((normalized.selected_school || '') && (normalized.selected_program || '')) {
try {
@ -245,50 +255,104 @@ const onProgramInput = async (e) => {
}, [careerId, id]);
async function handleSave(){
try{
// Compute chosen tuition exactly like Onboarding (manual override wins; blank => auto)
async function handleSave() {
try {
// Compute chosen tuition exactly like Onboarding (manual override wins; blank => auto)
const chosenTuition =
(manualTuition.trim() === '')
? autoTuition
: (Number.isFinite(parseFloat(manualTuition)) ? parseFloat(manualTuition) : autoTuition);
// Confirm user actually picked from list (one alert, on Save only)
const school = (form.selected_school || '').trim().toLowerCase();
const prog = (form.selected_program || '').trim().toLowerCase();
// validate against current server suggestions (not local files)
const exactSchool = school && schoolSug.find(o =>
(o.name || '').toLowerCase() === school
);
const schoolText = (form.selected_school || '').trim();
const progText = (form.selected_program || '').trim();
const school = schoolText.toLowerCase();
const prog = progText.toLowerCase();
if (school && !exactSchool) {
setSchoolValid(false);
alert('Please pick a school from the list.');
return;
}
const exactProgram = prog && progSug.find(p =>
(p.program || '').toLowerCase() === prog
);
if (prog && !exactProgram) {
setProgramValid(false);
alert('Please pick a program from the list.');
return;
}
const body = normalisePayload({ ...form, tuition: chosenTuition, career_profile_id: careerId, unit_id: selectedUnitId ?? null });
const res = await authFetch('/api/premium/college-profile',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
});
if(!res.ok) throw new Error(await res.text());
alert('Saved!');
setForm(p => ({ ...p, tuition: chosenTuition }));
setManualTuition(String(chosenTuition));
nav(-1);
}catch(err){ console.error(err); alert(err.message);}
// ---- SCHOOL validation ----
const exactSchoolLocal = school && schoolSug.find(o => (o.name || '').toLowerCase() === school);
const hasCanonical = !!selectedUnitId &&
canonicalSchoolName.current &&
canonicalSchoolName.current.toLowerCase() === school;
let exactSchool = exactSchoolLocal;
if (school && !exactSchool && !hasCanonical) {
// one-shot server confirm
try {
const resp = await authFetch(`/api/schools/suggest?query=${encodeURIComponent(schoolText)}&limit=50`);
const arr = resp.ok ? await resp.json() : [];
exactSchool = Array.isArray(arr)
? arr.find(o => (o.name || '').toLowerCase() === school)
: null;
if (exactSchool && !selectedUnitId) {
setSelectedUnitId(exactSchool.unitId ?? null);
canonicalSchoolName.current = exactSchool.name || schoolText;
}
} catch {}
}
if (school && !exactSchool && !hasCanonical) {
setSchoolValid(false);
alert('Please pick a school from the list.');
return;
}
// ---- PROGRAM validation ----
const progIsEmpty = progText === '';
if (progIsEmpty) {
setProgramValid(false);
alert('Please pick a program from the list.');
return;
}
let exactProgram = prog && progSug.find(p => (p.program || '').toLowerCase() === prog);
// One-shot server confirm if suggestions are empty/stale
if (!exactProgram) {
try {
const resp = await authFetch(
`/api/programs/suggest?school=${encodeURIComponent(schoolText)}&query=${encodeURIComponent(progText)}&limit=50`
);
const arr = resp.ok ? await resp.json() : [];
exactProgram = Array.isArray(arr)
? arr.find(p => (p.program || '').toLowerCase() === prog)
: null;
} catch {}
}
if (!exactProgram) {
setProgramValid(false);
alert('Please pick a program from the list.');
return;
}
// ---- SAVE ----
const body = normalisePayload({
...form,
tuition: chosenTuition,
career_profile_id: careerId,
unit_id: selectedUnitId ?? null
});
const res = await authFetch('/api/premium/college-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(await res.text());
alert('Saved!');
setForm(p => ({ ...p, tuition: chosenTuition }));
setManualTuition(String(chosenTuition));
nav(-1);
} catch (err) {
console.error(err);
alert(err.message);
}
}
useEffect(() => {
const sch = (form.selected_school || '').trim();
@ -329,7 +393,9 @@ const onProgramInput = async (e) => {
useEffect(() => {
(async () => {
const chpy = Number(form.credit_hours_per_year);
if (!selectedUnitId ||
const hasCanon = canonicalSchoolName.current &&
canonicalSchoolName.current.toLowerCase() === (form.selected_school || '').trim().toLowerCase();
if (!selectedUnitId || !hasCanon ||
!form.program_type ||
!Number.isFinite(chpy) ||
chpy <= 0) {
@ -355,6 +421,7 @@ useEffect(() => {
})();
}, [
selectedUnitId,
form.selected_school,
form.program_type,
form.credit_hours_per_year,
form.is_in_state,
@ -482,9 +549,13 @@ return (
setForm(prev => ({ ...prev, selected_school: exact.name }));
}
if (!selectedUnitId) setSelectedUnitId(exact.unitId ?? null);
canonicalSchoolName.current = exact.name;
setSchoolValid(true);
return;
}
// Valid if empty (still choosing) OR exact chosen
setSchoolValid(trimmed === '' || !!exact);
// Empty = neutral; any other text is invalid until committed from list
setSchoolValid(trimmed === '');
}}
list="school-suggestions"
className={`w-full border rounded p-2 ${
@ -513,7 +584,7 @@ return (
if (exact && form.selected_program !== exact.program) {
setForm(prev => ({ ...prev, selected_program: exact.program }));
}
setProgramValid(prog === '' || !!exact);
setProgramValid(prog === '' || !!exact);
}}
list="program-suggestions"
placeholder="Start typing and choose…"
@ -639,14 +710,26 @@ return (
className="w-full border rounded p-2"
/>
</div>
<span className="font-medium">Annual Aid</span>
<input
type="number"
name="annual_financial_aid"
value={form.annual_financial_aid}
onChange={handleFieldChange}
className="mt-1 w-full border rounded p-2"
/>
<div className="space-y-1">
<label className="block font-medium">(Estimated) Annual Financial Aid</label>
<div className="flex space-x-2">
<input
type="number"
name="annual_financial_aid"
value={form.annual_financial_aid}
onChange={handleFieldChange}
placeholder="e.g. 2000"
className="w-full border rounded p-2"
/>
<button
type="button"
onClick={() => setShowAidWizard(true)}
className="bg-blue-600 text-center px-3 py-2 rounded text-white"
>
Need Help?
</button>
</div>
</div>
{/* 8 │ Existing debt */}
@ -746,15 +829,26 @@ return (
>
 Back
</button>
<button
onClick={handleSave}
disabled={!schoolValid || !programValid}
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded"
<button
onClick={handleSave}
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded"
>
Save
</button>
</div>
{showAidWizard && (
<Modal onClose={() => setShowAidWizard(false)}>
<FinancialAidWizard
onAidEstimated={(estimate) => {
setForm(prev => ({ ...prev, annual_financial_aid: estimate }));
}}
onClose={() => setShowAidWizard(false)}
/>
</Modal>
)}
</div>
);

View File

@ -1,6 +1,21 @@
{
"status": "failed",
"failedTests": [
"d94173b0fe5d7002a306-47f9b330456659f0f977"
"912b0a42e830d5eb471e-760b803445f71997ff15",
"adebddef88bcf3522d03-5564093ce53787bc37f1",
"e2a1f72bade9c08182fe-6f621548b19be1d1c340",
"04d7e1cfdd54807256b0-d6ea376eb6511af71058",
"31db8689401acd273032-cab17a91a741a429f82d",
"1c59337757c0db6c5b5a-c3a2d557647a05580ec2",
"929c2cc6ba4f564b24fc-946b201d1d2ce3bcc831",
"929c2cc6ba4f564b24fc-bb3dcb00b3273979b065",
"d94173b0fe5d7002a306-4787dc08bfe1459dba5b",
"a5366403b9bfbbbe283e-0f6aea13931c9f9dd89f",
"a5366403b9bfbbbe283e-8723b5b1a3f4093effb0",
"c167e95522508c1da576-f44184408ded1c898957",
"37ddad175c38e79b0f15-93462299db1b1756eedc",
"37ddad175c38e79b0f15-50f35c78cf5a1a8b2635",
"ed5b94c6fed68d1ded5e-79dac3b701e35033b644",
"a22878cb937d50857944-f3a10ac1ede1d0305bc5"
]
}

View File

@ -0,0 +1,253 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- navigation [ref=e6]:
- button "Find Your Career" [ref=e8] [cursor=pointer]
- button "Preparing & UpSkilling for Your Career" [ref=e10] [cursor=pointer]
- button "Enhancing Your Career(Premium)" [ref=e12] [cursor=pointer]:
- text: Enhancing Your Career
- generic [ref=e13] [cursor=pointer]: (Premium)
- button "Retirement Planning (beta)(Premium)" [ref=e15] [cursor=pointer]:
- text: Retirement Planning (beta)
- generic [ref=e16] [cursor=pointer]: (Premium)
- button "Profile" [ref=e18] [cursor=pointer]
- generic [ref=e19]:
- button "Upgrade to Premium" [ref=e20] [cursor=pointer]
- button "Support" [ref=e21] [cursor=pointer]
- button "Logout" [ref=e22] [cursor=pointer]
- main [ref=e23]:
- generic [ref=e24]:
- generic [ref=e25]:
- heading "Explore Careers - use these tools to find your best fit" [level=2] [ref=e26]
- generic [ref=e27]:
- generic [ref=e28]:
- text: Search for Career
- generic [ref=e29]: "*"
- combobox "Start typing a career..." [ref=e31]
- paragraph [ref=e32]: Please pick from the dropdown when performing search. Our database is very comprehensive but cant accommodate every job title—choose the closest match to what youre searching for.
- generic [ref=e33]:
- heading "Career Comparison" [level=2] [ref=e34]
- button "Edit priorities" [ref=e35] [cursor=pointer]
- paragraph [ref=e36]: No careers added to comparison.
- generic [ref=e37]:
- combobox [ref=e38]:
- option "All Preparation Levels" [selected]
- option "Little or No Preparation"
- option "Some Preparation Needed"
- option "Medium Preparation Needed"
- option "Considerable Preparation Needed"
- option "Extensive Preparation Needed"
- combobox [ref=e39]:
- option "All Fit Levels" [selected]
- option "Best - Very Strong Match"
- option "Great - Strong Match"
- option "Good - Less Strong Match"
- button "Reload Career Suggestions" [active] [ref=e40] [cursor=pointer]
- generic [ref=e41]:
- generic [ref=e42]: ⚠️
- generic [ref=e43]: = May have limited data for this career path
- generic [ref=e44]:
- button "Amusement & Recreation Attendants ⚠️" [ref=e45] [cursor=pointer]:
- generic [ref=e46] [cursor=pointer]: Amusement & Recreation Attendants
- generic [ref=e47] [cursor=pointer]: ⚠️
- button "Baristas ⚠️" [ref=e48] [cursor=pointer]:
- generic [ref=e49] [cursor=pointer]: Baristas
- generic [ref=e50] [cursor=pointer]: ⚠️
- button "Bus Drivers, School" [ref=e51] [cursor=pointer]:
- generic [ref=e52] [cursor=pointer]: Bus Drivers, School
- button "Childcare Workers" [ref=e53] [cursor=pointer]:
- generic [ref=e54] [cursor=pointer]: Childcare Workers
- button "Coaches & Scouts" [ref=e55] [cursor=pointer]:
- generic [ref=e56] [cursor=pointer]: Coaches & Scouts
- button "Concierges" [ref=e57] [cursor=pointer]:
- generic [ref=e58] [cursor=pointer]: Concierges
- button "Exercise Trainers & Group Fitness Instructors" [ref=e59] [cursor=pointer]:
- generic [ref=e60] [cursor=pointer]: Exercise Trainers & Group Fitness Instructors
- button "Food Servers, Nonrestaurant ⚠️" [ref=e61] [cursor=pointer]:
- generic [ref=e62] [cursor=pointer]: Food Servers, Nonrestaurant
- generic [ref=e63] [cursor=pointer]: ⚠️
- button "Funeral Attendants ⚠️" [ref=e64] [cursor=pointer]:
- generic [ref=e65] [cursor=pointer]: Funeral Attendants
- generic [ref=e66] [cursor=pointer]: ⚠️
- button "Home Health Aides ⚠️" [ref=e67] [cursor=pointer]:
- generic [ref=e68] [cursor=pointer]: Home Health Aides
- generic [ref=e69] [cursor=pointer]: ⚠️
- button "Hosts & Hostesses, Restaurant, Lounge, & Coffee Shop ⚠️" [ref=e70] [cursor=pointer]:
- generic [ref=e71] [cursor=pointer]: Hosts & Hostesses, Restaurant, Lounge, & Coffee Shop
- generic [ref=e72] [cursor=pointer]: ⚠️
- button "Locker Room, Coatroom, & Dressing Room Attendants ⚠️" [ref=e73] [cursor=pointer]:
- generic [ref=e74] [cursor=pointer]: Locker Room, Coatroom, & Dressing Room Attendants
- generic [ref=e75] [cursor=pointer]: ⚠️
- button "Nannies" [ref=e76] [cursor=pointer]:
- generic [ref=e77] [cursor=pointer]: Nannies
- button "Nursing Assistants" [ref=e78] [cursor=pointer]:
- generic [ref=e79] [cursor=pointer]: Nursing Assistants
- button "Occupational Therapy Aides" [ref=e80] [cursor=pointer]:
- generic [ref=e81] [cursor=pointer]: Occupational Therapy Aides
- button "Passenger Attendants ⚠️" [ref=e82] [cursor=pointer]:
- generic [ref=e83] [cursor=pointer]: Passenger Attendants
- generic [ref=e84] [cursor=pointer]: ⚠️
- button "Personal Care Aides ⚠️" [ref=e85] [cursor=pointer]:
- generic [ref=e86] [cursor=pointer]: Personal Care Aides
- generic [ref=e87] [cursor=pointer]: ⚠️
- button "Physical Therapist Aides" [ref=e88] [cursor=pointer]:
- generic [ref=e89] [cursor=pointer]: Physical Therapist Aides
- button "Recreation Workers" [ref=e90] [cursor=pointer]:
- generic [ref=e91] [cursor=pointer]: Recreation Workers
- button "Residential Advisors" [ref=e92] [cursor=pointer]:
- generic [ref=e93] [cursor=pointer]: Residential Advisors
- button "School Bus Monitors ⚠️" [ref=e94] [cursor=pointer]:
- generic [ref=e95] [cursor=pointer]: School Bus Monitors
- generic [ref=e96] [cursor=pointer]: ⚠️
- button "Substitute Teachers, Short-Term ⚠️" [ref=e97] [cursor=pointer]:
- generic [ref=e98] [cursor=pointer]: Substitute Teachers, Short-Term
- generic [ref=e99] [cursor=pointer]: ⚠️
- button "Teaching Assistants, Preschool, Elementary, Middle, & Secondary School ⚠️" [ref=e100] [cursor=pointer]:
- generic [ref=e101] [cursor=pointer]: Teaching Assistants, Preschool, Elementary, Middle, & Secondary School
- generic [ref=e102] [cursor=pointer]: ⚠️
- button "Teaching Assistants, Special Education ⚠️" [ref=e103] [cursor=pointer]:
- generic [ref=e104] [cursor=pointer]: Teaching Assistants, Special Education
- generic [ref=e105] [cursor=pointer]: ⚠️
- button "Tour Guides & Escorts ⚠️" [ref=e106] [cursor=pointer]:
- generic [ref=e107] [cursor=pointer]: Tour Guides & Escorts
- generic [ref=e108] [cursor=pointer]: ⚠️
- button "Ushers, Lobby Attendants, & Ticket Takers ⚠️" [ref=e109] [cursor=pointer]:
- generic [ref=e110] [cursor=pointer]: Ushers, Lobby Attendants, & Ticket Takers
- generic [ref=e111] [cursor=pointer]: ⚠️
- button "Waiters & Waitresses ⚠️" [ref=e112] [cursor=pointer]:
- generic [ref=e113] [cursor=pointer]: Waiters & Waitresses
- generic [ref=e114] [cursor=pointer]: ⚠️
- button "Adapted Physical Education Specialists" [ref=e115] [cursor=pointer]:
- generic [ref=e116] [cursor=pointer]: Adapted Physical Education Specialists
- button "Adult Basic Education, Adult Secondary Education, & English as a Second Language Instructors" [ref=e117] [cursor=pointer]:
- generic [ref=e118] [cursor=pointer]: Adult Basic Education, Adult Secondary Education, & English as a Second Language Instructors
- button "Athletes & Sports Competitors" [ref=e119] [cursor=pointer]:
- generic [ref=e120] [cursor=pointer]: Athletes & Sports Competitors
- button "Baggage Porters & Bellhops ⚠️" [ref=e121] [cursor=pointer]:
- generic [ref=e122] [cursor=pointer]: Baggage Porters & Bellhops
- generic [ref=e123] [cursor=pointer]: ⚠️
- button "Barbers" [ref=e124] [cursor=pointer]:
- generic [ref=e125] [cursor=pointer]: Barbers
- button "Bartenders" [ref=e126] [cursor=pointer]:
- generic [ref=e127] [cursor=pointer]: Bartenders
- button "Bus Drivers, Transit & Intercity" [ref=e128] [cursor=pointer]:
- generic [ref=e129] [cursor=pointer]: Bus Drivers, Transit & Intercity
- button "Career/Technical Education Teachers, Middle School" [ref=e130] [cursor=pointer]:
- generic [ref=e131] [cursor=pointer]: Career/Technical Education Teachers, Middle School
- button "Career/Technical Education Teachers, Secondary School" [ref=e132] [cursor=pointer]:
- generic [ref=e133] [cursor=pointer]: Career/Technical Education Teachers, Secondary School
- button "Clergy" [ref=e134] [cursor=pointer]:
- generic [ref=e135] [cursor=pointer]: Clergy
- button "Cooks, Private Household" [ref=e136] [cursor=pointer]:
- generic [ref=e137] [cursor=pointer]: Cooks, Private Household
- button "Correctional Officers & Jailers" [ref=e138] [cursor=pointer]:
- generic [ref=e139] [cursor=pointer]: Correctional Officers & Jailers
- button "Dietetic Technicians" [ref=e140] [cursor=pointer]:
- generic [ref=e141] [cursor=pointer]: Dietetic Technicians
- button "Dining Room & Cafeteria Attendants & Bartender Helpers ⚠️" [ref=e142] [cursor=pointer]:
- generic [ref=e143] [cursor=pointer]: Dining Room & Cafeteria Attendants & Bartender Helpers
- generic [ref=e144] [cursor=pointer]: ⚠️
- button "Elementary School Teachers" [ref=e145] [cursor=pointer]:
- generic [ref=e146] [cursor=pointer]: Elementary School Teachers
- button "Fast Food & Counter Workers ⚠️" [ref=e147] [cursor=pointer]:
- generic [ref=e148] [cursor=pointer]: Fast Food & Counter Workers
- generic [ref=e149] [cursor=pointer]: ⚠️
- button "Fitness & Wellness Coordinators" [ref=e150] [cursor=pointer]:
- generic [ref=e151] [cursor=pointer]: Fitness & Wellness Coordinators
- button "Flight Attendants" [ref=e152] [cursor=pointer]:
- generic [ref=e153] [cursor=pointer]: Flight Attendants
- button "Hairdressers, Hairstylists, & Cosmetologists" [ref=e154] [cursor=pointer]:
- generic [ref=e155] [cursor=pointer]: Hairdressers, Hairstylists, & Cosmetologists
- button "Hotel, Motel, & Resort Desk Clerks ⚠️" [ref=e156] [cursor=pointer]:
- generic [ref=e157] [cursor=pointer]: Hotel, Motel, & Resort Desk Clerks
- generic [ref=e158] [cursor=pointer]: ⚠️
- button "Kindergarten Teachers" [ref=e159] [cursor=pointer]:
- generic [ref=e160] [cursor=pointer]: Kindergarten Teachers
- button "Licensed Practical & Licensed Vocational Nurses" [ref=e161] [cursor=pointer]:
- generic [ref=e162] [cursor=pointer]: Licensed Practical & Licensed Vocational Nurses
- button "Middle School Teachers" [ref=e163] [cursor=pointer]:
- generic [ref=e164] [cursor=pointer]: Middle School Teachers
- button "Midwives" [ref=e165] [cursor=pointer]:
- generic [ref=e166] [cursor=pointer]: Midwives
- button "Morticians, Undertakers, & Funeral Arrangers" [ref=e167] [cursor=pointer]:
- generic [ref=e168] [cursor=pointer]: Morticians, Undertakers, & Funeral Arrangers
- button "Occupational Therapy Assistants" [ref=e169] [cursor=pointer]:
- generic [ref=e170] [cursor=pointer]: Occupational Therapy Assistants
- button "Orderlies ⚠️" [ref=e171] [cursor=pointer]:
- generic [ref=e172] [cursor=pointer]: Orderlies
- generic [ref=e173] [cursor=pointer]: ⚠️
- button "Physical Therapist Assistants" [ref=e174] [cursor=pointer]:
- generic [ref=e175] [cursor=pointer]: Physical Therapist Assistants
- button "Preschool Teachers" [ref=e176] [cursor=pointer]:
- generic [ref=e177] [cursor=pointer]: Preschool Teachers
- button "Psychiatric Aides" [ref=e178] [cursor=pointer]:
- generic [ref=e179] [cursor=pointer]: Psychiatric Aides
- button "Reservation & Transportation Ticket Agents & Travel Clerks ⚠️" [ref=e180] [cursor=pointer]:
- generic [ref=e181] [cursor=pointer]: Reservation & Transportation Ticket Agents & Travel Clerks
- generic [ref=e182] [cursor=pointer]: ⚠️
- button "Secondary School Teachers" [ref=e183] [cursor=pointer]:
- generic [ref=e184] [cursor=pointer]: Secondary School Teachers
- button "Self-Enrichment Teachers" [ref=e185] [cursor=pointer]:
- generic [ref=e186] [cursor=pointer]: Self-Enrichment Teachers
- button "Shampooers" [ref=e187] [cursor=pointer]:
- generic [ref=e188] [cursor=pointer]: Shampooers
- button "Skincare Specialists" [ref=e189] [cursor=pointer]:
- generic [ref=e190] [cursor=pointer]: Skincare Specialists
- button "Social & Human Service Assistants" [ref=e191] [cursor=pointer]:
- generic [ref=e192] [cursor=pointer]: Social & Human Service Assistants
- button "Teaching Assistants, Postsecondary" [ref=e193] [cursor=pointer]:
- generic [ref=e194] [cursor=pointer]: Teaching Assistants, Postsecondary
- button "Telephone Operators ⚠️" [ref=e195] [cursor=pointer]:
- generic [ref=e196] [cursor=pointer]: Telephone Operators
- generic [ref=e197] [cursor=pointer]: ⚠️
- button "Travel Guides ⚠️" [ref=e198] [cursor=pointer]:
- generic [ref=e199] [cursor=pointer]: Travel Guides
- generic [ref=e200] [cursor=pointer]: ⚠️
- button "Cooks, Fast Food ⚠️" [ref=e201] [cursor=pointer]:
- generic [ref=e202] [cursor=pointer]: Cooks, Fast Food
- generic [ref=e203] [cursor=pointer]: ⚠️
- button "Dishwashers ⚠️" [ref=e204] [cursor=pointer]:
- generic [ref=e205] [cursor=pointer]: Dishwashers
- generic [ref=e206] [cursor=pointer]: ⚠️
- button "Door-to-Door Sales Workers, News & Street Vendors, & Related Workers ⚠️" [ref=e207] [cursor=pointer]:
- generic [ref=e208] [cursor=pointer]: Door-to-Door Sales Workers, News & Street Vendors, & Related Workers
- generic [ref=e209] [cursor=pointer]: ⚠️
- button "Educational, Guidance, & Career Counselors & Advisors" [ref=e210] [cursor=pointer]:
- generic [ref=e211] [cursor=pointer]: Educational, Guidance, & Career Counselors & Advisors
- button "Helpers--Painters, Paperhangers, Plasterers, & Stucco Masons ⚠️" [ref=e212] [cursor=pointer]:
- generic [ref=e213] [cursor=pointer]: Helpers--Painters, Paperhangers, Plasterers, & Stucco Masons
- generic [ref=e214] [cursor=pointer]: ⚠️
- button "Hospitalists" [ref=e215] [cursor=pointer]:
- generic [ref=e216] [cursor=pointer]: Hospitalists
- button "Low Vision Therapists, Orientation & Mobility Specialists, & Vision Rehabilitation Therapists" [ref=e217] [cursor=pointer]:
- generic [ref=e218] [cursor=pointer]: Low Vision Therapists, Orientation & Mobility Specialists, & Vision Rehabilitation Therapists
- button "Maids & Housekeeping Cleaners ⚠️" [ref=e219] [cursor=pointer]:
- generic [ref=e220] [cursor=pointer]: Maids & Housekeeping Cleaners
- generic [ref=e221] [cursor=pointer]: ⚠️
- button "Nurse Midwives" [ref=e222] [cursor=pointer]:
- generic [ref=e223] [cursor=pointer]: Nurse Midwives
- button "Special Education Teachers, Preschool" [ref=e224] [cursor=pointer]:
- generic [ref=e225] [cursor=pointer]: Special Education Teachers, Preschool
- button "Substance Abuse & Behavioral Disorder Counselors ⚠️" [ref=e226] [cursor=pointer]:
- generic [ref=e227] [cursor=pointer]: Substance Abuse & Behavioral Disorder Counselors
- generic [ref=e228] [cursor=pointer]: ⚠️
- generic [ref=e229]:
- text: This page includes information from
- link "O*NET OnLine" [ref=e230] [cursor=pointer]:
- /url: https://www.onetcenter.org
- text: by the U.S. Department of Labor, Employment & Training Administration (USDOL/ETA). Used under the
- link "CC BY 4.0 license" [ref=e231] [cursor=pointer]:
- /url: https://creativecommons.org/licenses/by/4.0/
- text: . **O*NET®** is a trademark of USDOL/ETA. Salary and employment data are enriched with resources from the
- link "Bureau of Labor Statistics" [ref=e232] [cursor=pointer]:
- /url: https://www.bls.gov
- text: and program information from the
- link "National Center for Education Statistics" [ref=e233] [cursor=pointer]:
- /url: https://nces.ed.gov
- text: .
- button "Open chat" [ref=e234] [cursor=pointer]:
- img [ref=e235] [cursor=pointer]
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -0,0 +1,253 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- navigation [ref=e6]:
- button "Find Your Career" [ref=e8] [cursor=pointer]
- button "Preparing & UpSkilling for Your Career" [ref=e10] [cursor=pointer]
- button "Enhancing Your Career(Premium)" [ref=e12] [cursor=pointer]:
- text: Enhancing Your Career
- generic [ref=e13] [cursor=pointer]: (Premium)
- button "Retirement Planning (beta)(Premium)" [ref=e15] [cursor=pointer]:
- text: Retirement Planning (beta)
- generic [ref=e16] [cursor=pointer]: (Premium)
- button "Profile" [ref=e18] [cursor=pointer]
- generic [ref=e19]:
- button "Upgrade to Premium" [ref=e20] [cursor=pointer]
- button "Support" [ref=e21] [cursor=pointer]
- button "Logout" [ref=e22] [cursor=pointer]
- main [ref=e23]:
- generic [ref=e24]:
- generic [ref=e25]:
- heading "Explore Careers - use these tools to find your best fit" [level=2] [ref=e26]
- generic [ref=e27]:
- generic [ref=e28]:
- text: Search for Career
- generic [ref=e29]: "*"
- combobox "Start typing a career..." [ref=e31]
- paragraph [ref=e32]: Please pick from the dropdown when performing search. Our database is very comprehensive but cant accommodate every job title—choose the closest match to what youre searching for.
- generic [ref=e33]:
- heading "Career Comparison" [level=2] [ref=e34]
- button "Edit priorities" [ref=e35] [cursor=pointer]
- paragraph [ref=e36]: No careers added to comparison.
- generic [ref=e37]:
- combobox [ref=e38]:
- option "All Preparation Levels" [selected]
- option "Little or No Preparation"
- option "Some Preparation Needed"
- option "Medium Preparation Needed"
- option "Considerable Preparation Needed"
- option "Extensive Preparation Needed"
- combobox [ref=e39]:
- option "All Fit Levels" [selected]
- option "Best - Very Strong Match"
- option "Great - Strong Match"
- option "Good - Less Strong Match"
- button "Reload Career Suggestions" [ref=e40] [cursor=pointer]
- generic [ref=e41]:
- generic [ref=e42]: ⚠️
- generic [ref=e43]: = May have limited data for this career path
- generic [ref=e44]:
- button "Amusement & Recreation Attendants ⚠️" [ref=e45] [cursor=pointer]:
- generic [ref=e46] [cursor=pointer]: Amusement & Recreation Attendants
- generic [ref=e47] [cursor=pointer]: ⚠️
- button "Baristas ⚠️" [ref=e48] [cursor=pointer]:
- generic [ref=e49] [cursor=pointer]: Baristas
- generic [ref=e50] [cursor=pointer]: ⚠️
- button "Bus Drivers, School" [ref=e51] [cursor=pointer]:
- generic [ref=e52] [cursor=pointer]: Bus Drivers, School
- button "Childcare Workers" [ref=e53] [cursor=pointer]:
- generic [ref=e54] [cursor=pointer]: Childcare Workers
- button "Coaches & Scouts" [ref=e55] [cursor=pointer]:
- generic [ref=e56] [cursor=pointer]: Coaches & Scouts
- button "Concierges" [ref=e57] [cursor=pointer]:
- generic [ref=e58] [cursor=pointer]: Concierges
- button "Exercise Trainers & Group Fitness Instructors" [ref=e59] [cursor=pointer]:
- generic [ref=e60] [cursor=pointer]: Exercise Trainers & Group Fitness Instructors
- button "Food Servers, Nonrestaurant ⚠️" [ref=e61] [cursor=pointer]:
- generic [ref=e62] [cursor=pointer]: Food Servers, Nonrestaurant
- generic [ref=e63] [cursor=pointer]: ⚠️
- button "Funeral Attendants ⚠️" [ref=e64] [cursor=pointer]:
- generic [ref=e65] [cursor=pointer]: Funeral Attendants
- generic [ref=e66] [cursor=pointer]: ⚠️
- button "Home Health Aides ⚠️" [ref=e67] [cursor=pointer]:
- generic [ref=e68] [cursor=pointer]: Home Health Aides
- generic [ref=e69] [cursor=pointer]: ⚠️
- button "Hosts & Hostesses, Restaurant, Lounge, & Coffee Shop ⚠️" [ref=e70] [cursor=pointer]:
- generic [ref=e71] [cursor=pointer]: Hosts & Hostesses, Restaurant, Lounge, & Coffee Shop
- generic [ref=e72] [cursor=pointer]: ⚠️
- button "Locker Room, Coatroom, & Dressing Room Attendants ⚠️" [ref=e73] [cursor=pointer]:
- generic [ref=e74] [cursor=pointer]: Locker Room, Coatroom, & Dressing Room Attendants
- generic [ref=e75] [cursor=pointer]: ⚠️
- button "Nannies" [ref=e76] [cursor=pointer]:
- generic [ref=e77] [cursor=pointer]: Nannies
- button "Nursing Assistants" [ref=e78] [cursor=pointer]:
- generic [ref=e79] [cursor=pointer]: Nursing Assistants
- button "Occupational Therapy Aides" [ref=e80] [cursor=pointer]:
- generic [ref=e81] [cursor=pointer]: Occupational Therapy Aides
- button "Passenger Attendants ⚠️" [ref=e82] [cursor=pointer]:
- generic [ref=e83] [cursor=pointer]: Passenger Attendants
- generic [ref=e84] [cursor=pointer]: ⚠️
- button "Personal Care Aides ⚠️" [ref=e85] [cursor=pointer]:
- generic [ref=e86] [cursor=pointer]: Personal Care Aides
- generic [ref=e87] [cursor=pointer]: ⚠️
- button "Physical Therapist Aides" [ref=e88] [cursor=pointer]:
- generic [ref=e89] [cursor=pointer]: Physical Therapist Aides
- button "Recreation Workers" [ref=e90] [cursor=pointer]:
- generic [ref=e91] [cursor=pointer]: Recreation Workers
- button "Residential Advisors" [ref=e92] [cursor=pointer]:
- generic [ref=e93] [cursor=pointer]: Residential Advisors
- button "School Bus Monitors ⚠️" [ref=e94] [cursor=pointer]:
- generic [ref=e95] [cursor=pointer]: School Bus Monitors
- generic [ref=e96] [cursor=pointer]: ⚠️
- button "Substitute Teachers, Short-Term ⚠️" [ref=e97] [cursor=pointer]:
- generic [ref=e98] [cursor=pointer]: Substitute Teachers, Short-Term
- generic [ref=e99] [cursor=pointer]: ⚠️
- button "Teaching Assistants, Preschool, Elementary, Middle, & Secondary School ⚠️" [ref=e100] [cursor=pointer]:
- generic [ref=e101] [cursor=pointer]: Teaching Assistants, Preschool, Elementary, Middle, & Secondary School
- generic [ref=e102] [cursor=pointer]: ⚠️
- button "Teaching Assistants, Special Education ⚠️" [ref=e103] [cursor=pointer]:
- generic [ref=e104] [cursor=pointer]: Teaching Assistants, Special Education
- generic [ref=e105] [cursor=pointer]: ⚠️
- button "Tour Guides & Escorts ⚠️" [ref=e106] [cursor=pointer]:
- generic [ref=e107] [cursor=pointer]: Tour Guides & Escorts
- generic [ref=e108] [cursor=pointer]: ⚠️
- button "Ushers, Lobby Attendants, & Ticket Takers ⚠️" [ref=e109] [cursor=pointer]:
- generic [ref=e110] [cursor=pointer]: Ushers, Lobby Attendants, & Ticket Takers
- generic [ref=e111] [cursor=pointer]: ⚠️
- button "Waiters & Waitresses ⚠️" [ref=e112] [cursor=pointer]:
- generic [ref=e113] [cursor=pointer]: Waiters & Waitresses
- generic [ref=e114] [cursor=pointer]: ⚠️
- button "Adapted Physical Education Specialists" [ref=e115] [cursor=pointer]:
- generic [ref=e116] [cursor=pointer]: Adapted Physical Education Specialists
- button "Adult Basic Education, Adult Secondary Education, & English as a Second Language Instructors" [ref=e117] [cursor=pointer]:
- generic [ref=e118] [cursor=pointer]: Adult Basic Education, Adult Secondary Education, & English as a Second Language Instructors
- button "Athletes & Sports Competitors" [ref=e119] [cursor=pointer]:
- generic [ref=e120] [cursor=pointer]: Athletes & Sports Competitors
- button "Baggage Porters & Bellhops ⚠️" [ref=e121] [cursor=pointer]:
- generic [ref=e122] [cursor=pointer]: Baggage Porters & Bellhops
- generic [ref=e123] [cursor=pointer]: ⚠️
- button "Barbers" [ref=e124] [cursor=pointer]:
- generic [ref=e125] [cursor=pointer]: Barbers
- button "Bartenders" [ref=e126] [cursor=pointer]:
- generic [ref=e127] [cursor=pointer]: Bartenders
- button "Bus Drivers, Transit & Intercity" [ref=e128] [cursor=pointer]:
- generic [ref=e129] [cursor=pointer]: Bus Drivers, Transit & Intercity
- button "Career/Technical Education Teachers, Middle School" [ref=e130] [cursor=pointer]:
- generic [ref=e131] [cursor=pointer]: Career/Technical Education Teachers, Middle School
- button "Career/Technical Education Teachers, Secondary School" [ref=e132] [cursor=pointer]:
- generic [ref=e133] [cursor=pointer]: Career/Technical Education Teachers, Secondary School
- button "Clergy" [ref=e134] [cursor=pointer]:
- generic [ref=e135] [cursor=pointer]: Clergy
- button "Cooks, Private Household" [ref=e136] [cursor=pointer]:
- generic [ref=e137] [cursor=pointer]: Cooks, Private Household
- button "Correctional Officers & Jailers" [ref=e138] [cursor=pointer]:
- generic [ref=e139] [cursor=pointer]: Correctional Officers & Jailers
- button "Dietetic Technicians" [ref=e140] [cursor=pointer]:
- generic [ref=e141] [cursor=pointer]: Dietetic Technicians
- button "Dining Room & Cafeteria Attendants & Bartender Helpers ⚠️" [ref=e142] [cursor=pointer]:
- generic [ref=e143] [cursor=pointer]: Dining Room & Cafeteria Attendants & Bartender Helpers
- generic [ref=e144] [cursor=pointer]: ⚠️
- button "Elementary School Teachers" [ref=e145] [cursor=pointer]:
- generic [ref=e146] [cursor=pointer]: Elementary School Teachers
- button "Fast Food & Counter Workers ⚠️" [ref=e147] [cursor=pointer]:
- generic [ref=e148] [cursor=pointer]: Fast Food & Counter Workers
- generic [ref=e149] [cursor=pointer]: ⚠️
- button "Fitness & Wellness Coordinators" [ref=e150] [cursor=pointer]:
- generic [ref=e151] [cursor=pointer]: Fitness & Wellness Coordinators
- button "Flight Attendants" [ref=e152] [cursor=pointer]:
- generic [ref=e153] [cursor=pointer]: Flight Attendants
- button "Hairdressers, Hairstylists, & Cosmetologists" [ref=e154] [cursor=pointer]:
- generic [ref=e155] [cursor=pointer]: Hairdressers, Hairstylists, & Cosmetologists
- button "Hotel, Motel, & Resort Desk Clerks ⚠️" [ref=e156] [cursor=pointer]:
- generic [ref=e157] [cursor=pointer]: Hotel, Motel, & Resort Desk Clerks
- generic [ref=e158] [cursor=pointer]: ⚠️
- button "Kindergarten Teachers" [ref=e159] [cursor=pointer]:
- generic [ref=e160] [cursor=pointer]: Kindergarten Teachers
- button "Licensed Practical & Licensed Vocational Nurses" [ref=e161] [cursor=pointer]:
- generic [ref=e162] [cursor=pointer]: Licensed Practical & Licensed Vocational Nurses
- button "Middle School Teachers" [ref=e163] [cursor=pointer]:
- generic [ref=e164] [cursor=pointer]: Middle School Teachers
- button "Midwives" [ref=e165] [cursor=pointer]:
- generic [ref=e166] [cursor=pointer]: Midwives
- button "Morticians, Undertakers, & Funeral Arrangers" [ref=e167] [cursor=pointer]:
- generic [ref=e168] [cursor=pointer]: Morticians, Undertakers, & Funeral Arrangers
- button "Occupational Therapy Assistants" [ref=e169] [cursor=pointer]:
- generic [ref=e170] [cursor=pointer]: Occupational Therapy Assistants
- button "Orderlies ⚠️" [ref=e171] [cursor=pointer]:
- generic [ref=e172] [cursor=pointer]: Orderlies
- generic [ref=e173] [cursor=pointer]: ⚠️
- button "Physical Therapist Assistants" [ref=e174] [cursor=pointer]:
- generic [ref=e175] [cursor=pointer]: Physical Therapist Assistants
- button "Preschool Teachers" [ref=e176] [cursor=pointer]:
- generic [ref=e177] [cursor=pointer]: Preschool Teachers
- button "Psychiatric Aides" [ref=e178] [cursor=pointer]:
- generic [ref=e179] [cursor=pointer]: Psychiatric Aides
- button "Reservation & Transportation Ticket Agents & Travel Clerks ⚠️" [ref=e180] [cursor=pointer]:
- generic [ref=e181] [cursor=pointer]: Reservation & Transportation Ticket Agents & Travel Clerks
- generic [ref=e182] [cursor=pointer]: ⚠️
- button "Secondary School Teachers" [ref=e183] [cursor=pointer]:
- generic [ref=e184] [cursor=pointer]: Secondary School Teachers
- button "Self-Enrichment Teachers" [ref=e185] [cursor=pointer]:
- generic [ref=e186] [cursor=pointer]: Self-Enrichment Teachers
- button "Shampooers" [ref=e187] [cursor=pointer]:
- generic [ref=e188] [cursor=pointer]: Shampooers
- button "Skincare Specialists" [ref=e189] [cursor=pointer]:
- generic [ref=e190] [cursor=pointer]: Skincare Specialists
- button "Social & Human Service Assistants" [ref=e191] [cursor=pointer]:
- generic [ref=e192] [cursor=pointer]: Social & Human Service Assistants
- button "Teaching Assistants, Postsecondary" [ref=e193] [cursor=pointer]:
- generic [ref=e194] [cursor=pointer]: Teaching Assistants, Postsecondary
- button "Telephone Operators ⚠️" [ref=e195] [cursor=pointer]:
- generic [ref=e196] [cursor=pointer]: Telephone Operators
- generic [ref=e197] [cursor=pointer]: ⚠️
- button "Travel Guides ⚠️" [ref=e198] [cursor=pointer]:
- generic [ref=e199] [cursor=pointer]: Travel Guides
- generic [ref=e200] [cursor=pointer]: ⚠️
- button "Cooks, Fast Food ⚠️" [ref=e201] [cursor=pointer]:
- generic [ref=e202] [cursor=pointer]: Cooks, Fast Food
- generic [ref=e203] [cursor=pointer]: ⚠️
- button "Dishwashers ⚠️" [ref=e204] [cursor=pointer]:
- generic [ref=e205] [cursor=pointer]: Dishwashers
- generic [ref=e206] [cursor=pointer]: ⚠️
- button "Door-to-Door Sales Workers, News & Street Vendors, & Related Workers ⚠️" [ref=e207] [cursor=pointer]:
- generic [ref=e208] [cursor=pointer]: Door-to-Door Sales Workers, News & Street Vendors, & Related Workers
- generic [ref=e209] [cursor=pointer]: ⚠️
- button "Educational, Guidance, & Career Counselors & Advisors" [ref=e210] [cursor=pointer]:
- generic [ref=e211] [cursor=pointer]: Educational, Guidance, & Career Counselors & Advisors
- button "Helpers--Painters, Paperhangers, Plasterers, & Stucco Masons ⚠️" [ref=e212] [cursor=pointer]:
- generic [ref=e213] [cursor=pointer]: Helpers--Painters, Paperhangers, Plasterers, & Stucco Masons
- generic [ref=e214] [cursor=pointer]: ⚠️
- button "Hospitalists" [ref=e215] [cursor=pointer]:
- generic [ref=e216] [cursor=pointer]: Hospitalists
- button "Low Vision Therapists, Orientation & Mobility Specialists, & Vision Rehabilitation Therapists" [ref=e217] [cursor=pointer]:
- generic [ref=e218] [cursor=pointer]: Low Vision Therapists, Orientation & Mobility Specialists, & Vision Rehabilitation Therapists
- button "Maids & Housekeeping Cleaners ⚠️" [ref=e219] [cursor=pointer]:
- generic [ref=e220] [cursor=pointer]: Maids & Housekeeping Cleaners
- generic [ref=e221] [cursor=pointer]: ⚠️
- button "Nurse Midwives" [ref=e222] [cursor=pointer]:
- generic [ref=e223] [cursor=pointer]: Nurse Midwives
- button "Special Education Teachers, Preschool" [ref=e224] [cursor=pointer]:
- generic [ref=e225] [cursor=pointer]: Special Education Teachers, Preschool
- button "Substance Abuse & Behavioral Disorder Counselors ⚠️" [ref=e226] [cursor=pointer]:
- generic [ref=e227] [cursor=pointer]: Substance Abuse & Behavioral Disorder Counselors
- generic [ref=e228] [cursor=pointer]: ⚠️
- generic [ref=e229]:
- text: This page includes information from
- link "O*NET OnLine" [ref=e230] [cursor=pointer]:
- /url: https://www.onetcenter.org
- text: by the U.S. Department of Labor, Employment & Training Administration (USDOL/ETA). Used under the
- link "CC BY 4.0 license" [ref=e231] [cursor=pointer]:
- /url: https://creativecommons.org/licenses/by/4.0/
- text: . **O*NET®** is a trademark of USDOL/ETA. Salary and employment data are enriched with resources from the
- link "Bureau of Labor Statistics" [ref=e232] [cursor=pointer]:
- /url: https://www.bls.gov
- text: and program information from the
- link "National Center for Education Statistics" [ref=e233] [cursor=pointer]:
- /url: https://nces.ed.gov
- text: .
- button "Open chat" [ref=e234] [cursor=pointer]:
- img [ref=e235] [cursor=pointer]
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -0,0 +1,34 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- navigation [ref=e6]:
- button "Find Your Career" [ref=e8] [cursor=pointer]
- button "Preparing & UpSkilling for Your Career" [ref=e10] [cursor=pointer]
- button "Enhancing Your Career(Premium)" [ref=e12] [cursor=pointer]:
- text: Enhancing Your Career
- generic [ref=e13] [cursor=pointer]: (Premium)
- button "Retirement Planning (beta)(Premium)" [ref=e15] [cursor=pointer]:
- text: Retirement Planning (beta)
- generic [ref=e16] [cursor=pointer]: (Premium)
- button "Profile" [ref=e18] [cursor=pointer]
- generic [ref=e19]:
- button "Upgrade to Premium" [ref=e20] [cursor=pointer]
- button "Support" [ref=e21] [cursor=pointer]
- button "Logout" [ref=e22] [cursor=pointer]
- main [ref=e23]:
- generic [ref=e24]:
- heading "Educational Programs" [level=2] [ref=e25]
- paragraph [ref=e26]: "You have not selected a career yet. Please search for one below:"
- generic [ref=e27]:
- generic [ref=e28]:
- text: Search for Career
- generic [ref=e29]: "*"
- combobox "Start typing a career..." [ref=e31]
- paragraph [ref=e32]: Please pick from the dropdown when performing search. Our database is very comprehensive but cant accommodate every job title—choose the closest match to what youre searching for.
- paragraph [ref=e33]: After you pick a career, well display matching educational programs.
- button "Open chat" [ref=e34] [cursor=pointer]:
- img [ref=e35] [cursor=pointer]
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -0,0 +1,34 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- navigation [ref=e6]:
- button "Find Your Career" [ref=e8] [cursor=pointer]
- button "Preparing & UpSkilling for Your Career" [ref=e10] [cursor=pointer]
- button "Enhancing Your Career(Premium)" [ref=e12] [cursor=pointer]:
- text: Enhancing Your Career
- generic [ref=e13] [cursor=pointer]: (Premium)
- button "Retirement Planning (beta)(Premium)" [ref=e15] [cursor=pointer]:
- text: Retirement Planning (beta)
- generic [ref=e16] [cursor=pointer]: (Premium)
- button "Profile" [ref=e18] [cursor=pointer]
- generic [ref=e19]:
- button "Upgrade to Premium" [ref=e20] [cursor=pointer]
- button "Support" [ref=e21] [cursor=pointer]
- button "Logout" [ref=e22] [cursor=pointer]
- main [ref=e23]:
- generic [ref=e24]:
- heading "Educational Programs" [level=2] [ref=e25]
- paragraph [ref=e26]: "You have not selected a career yet. Please search for one below:"
- generic [ref=e27]:
- generic [ref=e28]:
- text: Search for Career
- generic [ref=e29]: "*"
- combobox "Start typing a career..." [ref=e31]
- paragraph [ref=e32]: Please pick from the dropdown when performing search. Our database is very comprehensive but cant accommodate every job title—choose the closest match to what youre searching for.
- paragraph [ref=e33]: After you pick a career, well display matching educational programs.
- button "Open chat" [ref=e34] [cursor=pointer]:
- img [ref=e35] [cursor=pointer]
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -0,0 +1,92 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- navigation [ref=e6]:
- button "Find Your Career" [ref=e8] [cursor=pointer]
- button "Preparing & UpSkilling for Your Career" [ref=e10] [cursor=pointer]
- button "Enhancing Your Career(Premium)" [ref=e12] [cursor=pointer]:
- text: Enhancing Your Career
- generic [ref=e13] [cursor=pointer]: (Premium)
- button "Retirement Planning (beta)(Premium)" [ref=e15] [cursor=pointer]:
- text: Retirement Planning (beta)
- generic [ref=e16] [cursor=pointer]: (Premium)
- button "Profile" [ref=e18] [cursor=pointer]
- generic [ref=e19]:
- button "Upgrade to Premium" [ref=e20] [cursor=pointer]
- button "Support" [ref=e21] [cursor=pointer]
- button "Logout" [ref=e22] [cursor=pointer]
- main [ref=e23]:
- generic [ref=e24]:
- generic [ref=e25]:
- heading "Explore Careers - use these tools to find your best fit" [level=2] [ref=e26]
- generic [ref=e27]:
- generic [ref=e28]:
- text: Search for Career
- generic [ref=e29]: "*"
- combobox "Start typing a career..." [active] [ref=e31]: cu
- paragraph [ref=e32]: Please pick from the dropdown when performing search. Our database is very comprehensive but cant accommodate every job title—choose the closest match to what youre searching for.
- generic [ref=e33]:
- heading "Career Comparison" [level=2] [ref=e34]
- button "Edit priorities" [ref=e35] [cursor=pointer]
- table [ref=e36]:
- rowgroup [ref=e37]:
- row "Career interests meaning stability growth balance recognition Match Actions" [ref=e38]:
- cell "Career" [ref=e39]
- cell "interests" [ref=e40]
- cell "meaning" [ref=e41]
- cell "stability" [ref=e42]
- cell "growth" [ref=e43]
- cell "balance" [ref=e44]
- cell "recognition" [ref=e45]
- cell "Match" [ref=e46]
- cell "Actions" [ref=e47]
- rowgroup [ref=e48]:
- row "Amusement & Recreation Attendants 5 3 1 3 3 3 53.8% Remove Plan your Education/Skills" [ref=e49]:
- cell "Amusement & Recreation Attendants" [ref=e50]
- cell "5" [ref=e51]
- cell "3" [ref=e52]
- cell "1" [ref=e53]
- cell "3" [ref=e54]
- cell "3" [ref=e55]
- cell "3" [ref=e56]
- cell "53.8%" [ref=e57]
- cell "Remove Plan your Education/Skills" [ref=e58]:
- button "Remove" [ref=e59] [cursor=pointer]
- button "Plan your Education/Skills" [ref=e60] [cursor=pointer]
- generic [ref=e61]:
- combobox [ref=e62]:
- option "All Preparation Levels" [selected]
- option "Little or No Preparation"
- option "Some Preparation Needed"
- option "Medium Preparation Needed"
- option "Considerable Preparation Needed"
- option "Extensive Preparation Needed"
- combobox [ref=e63]:
- option "All Fit Levels" [selected]
- option "Best - Very Strong Match"
- option "Great - Strong Match"
- option "Good - Less Strong Match"
- button "Reload Career Suggestions" [ref=e64] [cursor=pointer]
- generic [ref=e65]:
- generic [ref=e66]: ⚠️
- generic [ref=e67]: = May have limited data for this career path
- generic [ref=e69]:
- text: This page includes information from
- link "O*NET OnLine" [ref=e70] [cursor=pointer]:
- /url: https://www.onetcenter.org
- text: by the U.S. Department of Labor, Employment & Training Administration (USDOL/ETA). Used under the
- link "CC BY 4.0 license" [ref=e71] [cursor=pointer]:
- /url: https://creativecommons.org/licenses/by/4.0/
- text: . **O*NET®** is a trademark of USDOL/ETA. Salary and employment data are enriched with resources from the
- link "Bureau of Labor Statistics" [ref=e72] [cursor=pointer]:
- /url: https://www.bls.gov
- text: and program information from the
- link "National Center for Education Statistics" [ref=e73] [cursor=pointer]:
- /url: https://nces.ed.gov
- text: .
- button "Open chat" [ref=e74] [cursor=pointer]:
- img [ref=e75] [cursor=pointer]
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -0,0 +1,22 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e8]:
- heading "Sign In" [level=1] [ref=e9]
- paragraph [ref=e10]: Unexpected token 'T', "Too many r"... is not valid JSON
- generic [ref=e11]:
- textbox "Username" [ref=e12]: test_u202509111029199314
- textbox "Password" [ref=e13]: P@ssw0rd!9314
- button "Sign In" [active] [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,23 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e7]:
- generic [ref=e8]: Your session has expired. Please sign in again.
- generic [ref=e9]:
- heading "Sign In" [level=1] [ref=e10]
- generic [ref=e11]:
- textbox "Username" [ref=e12]
- textbox "Password" [ref=e13]
- button "Sign In" [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,23 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e7]:
- generic [ref=e8]: Your session has expired. Please sign in again.
- generic [ref=e9]:
- heading "Sign In" [level=1] [ref=e10]
- generic [ref=e11]:
- textbox "Username" [ref=e12]
- textbox "Password" [ref=e13]
- button "Sign In" [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,22 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e8]:
- heading "Sign In" [level=1] [ref=e9]
- paragraph [ref=e10]: Unexpected token 'T', "Too many r"... is not valid JSON
- generic [ref=e11]:
- textbox "Username" [ref=e12]: test_u202509111029199314
- textbox "Password" [ref=e13]: P@ssw0rd!9314
- button "Sign In" [active] [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,50 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e2]:
- navigation "Navigation Bar" [ref=e3]:
- generic [ref=e4]:
- link "Home" [ref=e5] [cursor=pointer]:
- /url: /
- img [ref=e6] [cursor=pointer]
- link "Explore" [ref=e7] [cursor=pointer]:
- /url: /explore/repos
- link "Help" [ref=e8] [cursor=pointer]:
- /url: https://docs.gitea.com
- generic [ref=e9]:
- link "Register" [ref=e10] [cursor=pointer]:
- /url: /user/sign_up
- img [ref=e11] [cursor=pointer]
- text: Register
- link "Sign In" [ref=e13] [cursor=pointer]:
- /url: /user/login?redirect_to=%2fsignin
- img [ref=e14] [cursor=pointer]
- text: Sign In
- main "Page Not Found" [ref=e16]:
- generic [ref=e17]:
- img "404" [ref=e18]
- paragraph [ref=e19]:
- text: The page you are trying to reach either
- strong [ref=e20]: does not exist
- text: or
- strong [ref=e21]: you are not authorized
- text: to view it.
- group "Footer" [ref=e22]:
- contentinfo "About Software" [ref=e23]:
- link "Powered by Gitea" [ref=e24] [cursor=pointer]:
- /url: https://about.gitea.com
- text: "Version: 1.22.6 Page:"
- strong [ref=e25]: 2ms
- text: "Template:"
- strong [ref=e26]: 1ms
- group "Links" [ref=e27]:
- menu [ref=e28] [cursor=pointer]:
- generic [ref=e29] [cursor=pointer]:
- img [ref=e30] [cursor=pointer]
- text: English
- link "Licenses" [ref=e32] [cursor=pointer]:
- /url: /assets/licenses.txt
- link "API" [ref=e33] [cursor=pointer]:
- /url: /api/swagger
```

View File

@ -1,50 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e2]:
- navigation "Navigation Bar" [ref=e3]:
- generic [ref=e4]:
- link "Home" [ref=e5] [cursor=pointer]:
- /url: /
- img [ref=e6] [cursor=pointer]
- link "Explore" [ref=e7] [cursor=pointer]:
- /url: /explore/repos
- link "Help" [ref=e8] [cursor=pointer]:
- /url: https://docs.gitea.com
- generic [ref=e9]:
- link "Register" [ref=e10] [cursor=pointer]:
- /url: /user/sign_up
- img [ref=e11] [cursor=pointer]
- text: Register
- link "Sign In" [ref=e13] [cursor=pointer]:
- /url: /user/login?redirect_to=%2fsignin
- img [ref=e14] [cursor=pointer]
- text: Sign In
- main "Page Not Found" [ref=e16]:
- generic [ref=e17]:
- img "404" [ref=e18]
- paragraph [ref=e19]:
- text: The page you are trying to reach either
- strong [ref=e20]: does not exist
- text: or
- strong [ref=e21]: you are not authorized
- text: to view it.
- group "Footer" [ref=e22]:
- contentinfo "About Software" [ref=e23]:
- link "Powered by Gitea" [ref=e24] [cursor=pointer]:
- /url: https://about.gitea.com
- text: "Version: 1.22.6 Page:"
- strong [ref=e25]: 8ms
- text: "Template:"
- strong [ref=e26]: 6ms
- group "Links" [ref=e27]:
- menu [ref=e28] [cursor=pointer]:
- generic [ref=e29] [cursor=pointer]:
- img [ref=e30] [cursor=pointer]
- text: English
- link "Licenses" [ref=e32] [cursor=pointer]:
- /url: /assets/licenses.txt
- link "API" [ref=e33] [cursor=pointer]:
- /url: /api/swagger
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,22 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e8]:
- heading "Sign In" [level=1] [ref=e9]
- paragraph [ref=e10]: Unexpected token 'T', "Too many r"... is not valid JSON
- generic [ref=e11]:
- textbox "Username" [ref=e12]: test_u202509111029199314
- textbox "Password" [ref=e13]: P@ssw0rd!9314
- button "Sign In" [active] [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,22 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e8]:
- heading "Sign In" [level=1] [ref=e9]
- paragraph [ref=e10]: Unexpected token 'T', "Too many r"... is not valid JSON
- generic [ref=e11]:
- textbox "Username" [ref=e12]: test_u202509111029199314
- textbox "Password" [ref=e13]: P@ssw0rd!9314
- button "Sign In" [active] [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,22 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e8]:
- heading "Sign In" [level=1] [ref=e9]
- paragraph [ref=e10]: Unexpected token 'T', "Too many r"... is not valid JSON
- generic [ref=e11]:
- textbox "Username" [ref=e12]: test_u202509111029199314
- textbox "Password" [ref=e13]: P@ssw0rd!9314
- button "Sign In" [active] [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,22 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e8]:
- heading "Sign In" [level=1] [ref=e9]
- paragraph [ref=e10]: Unexpected token 'T', "Too many r"... is not valid JSON
- generic [ref=e11]:
- textbox "Username" [ref=e12]: test_u202509111029199314
- textbox "Password" [ref=e13]: P@ssw0rd!9314
- button "Sign In" [active] [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,22 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e8]:
- heading "Sign In" [level=1] [ref=e9]
- paragraph [ref=e10]: Unexpected token 'T', "Too many r"... is not valid JSON
- generic [ref=e11]:
- textbox "Username" [ref=e12]: test_u202509111029199314
- textbox "Password" [ref=e13]: P@ssw0rd!9314
- button "Sign In" [active] [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,22 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- main [ref=e6]:
- generic [ref=e8]:
- heading "Sign In" [level=1] [ref=e9]
- paragraph [ref=e10]: Unexpected token 'T', "Too many r"... is not valid JSON
- generic [ref=e11]:
- textbox "Username" [ref=e12]: test_u202509111029199314
- textbox "Password" [ref=e13]: P@ssw0rd!9314
- button "Sign In" [active] [ref=e14] [cursor=pointer]
- generic [ref=e15]:
- paragraph [ref=e16]:
- text: Dont have an account?
- link "Sign Up" [ref=e17] [cursor=pointer]:
- /url: /signup
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
- /url: /forgot-password
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,28 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- heading "AptivaAI - Career Guidance Platform" [level=1] [ref=e5]
- navigation [ref=e6]:
- button "Find Your Career" [ref=e8] [cursor=pointer]
- button "Preparing & UpSkilling for Your Career" [ref=e10] [cursor=pointer]
- button "Enhancing Your Career(Premium)" [ref=e12] [cursor=pointer]:
- text: Enhancing Your Career
- generic [ref=e13] [cursor=pointer]: (Premium)
- button "Retirement Planning (beta)(Premium)" [ref=e15] [cursor=pointer]:
- text: Retirement Planning (beta)
- generic [ref=e16] [cursor=pointer]: (Premium)
- button "Profile" [ref=e18] [cursor=pointer]
- generic [ref=e19]:
- button "Support" [ref=e20] [cursor=pointer]
- button "Logout" [ref=e21] [cursor=pointer]
- main [ref=e22]:
- generic [ref=e23]:
- 'heading "Your plan: Premium" [level=2] [ref=e24]'
- paragraph [ref=e25]: Manage payment method, invoices or cancel anytime.
- button "Manage subscription" [ref=e26] [cursor=pointer]
- button "Back to app" [ref=e27] [cursor=pointer]
- button "Open chat" [ref=e28] [cursor=pointer]:
- img [ref=e29] [cursor=pointer]
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -8,7 +8,7 @@ function uniq() {
}
test.describe('@p0 SignUp → Journey select → Route', () => {
test.setTimeout(15000); // allow for slower first load + areas fetch
test.setTimeout(10000); // allow for slower first load + areas fetch
test('create a new user via UI and persist creds for later specs', async ({ page }) => {
const u = uniq();

View File

@ -4,7 +4,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 SignIn → Landing', () => {
test.setTimeout(20000);
test.setTimeout(10000);
test('signs in with persisted user and reaches SignInLanding', async ({ page }) => {
const user = loadTestUser();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p1 Career Explorer — CareerSearch datalist', () => {
test.setTimeout(30000);
test.setTimeout(20000);
test('datalist commit opens modal; Change resets input', async ({ page }) => {
const user = loadTestUser();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Comparison: add → duplicate blocked → remove → persists', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('add one, block duplicate, remove and persist', async ({ page }) => {
const user = loadTestUser();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Priorities modal — save & persist (current UI)', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('open → choose answers → Save Answers → reload → same answers present', async ({ page }) => {
const user = loadTestUser();

View File

@ -4,7 +4,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Forgot Password — exact UI flow', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('SignIn → Forgot → invalid email blocked → valid submit shows success copy', async ({ page }) => {
const user = loadTestUser();

View File

@ -2,7 +2,7 @@
import { test, expect } from '@playwright/test';
test.describe('@p0 Reset Password — exact UI flow', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('mismatch passwords → inline error, no request', async ({ page }) => {
// Route with a token param (the component checks presence only)

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 PremiumRoute guard', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('non-premium user is redirected to /paywall from premium routes', async ({ page }) => {
const user = loadTestUser();

View File

@ -2,7 +2,7 @@
import { test, expect } from '@playwright/test';
test.describe('@p0 SessionExpiredHandler', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('unauth → protected route → /signin?session=expired + banner', async ({ page }) => {
await page.context().clearCookies();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Support modal — open/close', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('header Support opens modal and can be closed', async ({ page }) => {
const user = loadTestUser();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Paywall CTA', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('Upgrade to Premium visible on non-premium pages and navigates to /paywall', async ({ page }) => {
const user = loadTestUser();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Support — submit ticket', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('open → fill form → submit → success (network or UI)', async ({ page }) => {
const user = loadTestUser();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Header nav menus', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('Find Your Career menu → Career Explorer & Interest Inventory', async ({ page }) => {
const u = loadTestUser();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Profile menu gating (non-premium)', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('Career/College Profiles show disabled labels when user is not premium', async ({ page }) => {
const u = loadTestUser();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Support — burst rate limit', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('rapid submissions eventually return 429 Too Many Requests', async ({ page }) => {
const user = loadTestUser();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Support — auth + dedupe', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('unauthenticated request is rejected (401)', async ({ page }) => {
await page.context().clearCookies();

View File

@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Educational Programs — handoff & page render', () => {
test.setTimeout(20000);
test.setTimeout(15000);
test('handoff carries selectedCareer; page shows career title, KSA header and school cards; survives reload', async ({ page }) => {
const user = loadTestUser();

View File

@ -0,0 +1,166 @@
// @ts-check
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
// Grab the first input immediately following a label that matches text
// Use stable name-based locators (matches your component code)
const byName = (page, name) => page.locator(`input[name="${name}"]`).first();
// Fill a date input reliably (type=date => yyyy-mm-dd; else mm/dd/yyyy)
async function fillDateField(ctrl, iso = '2025-09-01', us = '09/01/2025') {
const type = (await ctrl.getAttribute('type')) || '';
if (type.toLowerCase() === 'date') {
await ctrl.fill(iso); // browser renders mm/dd/yyyy
} else {
await ctrl.scrollIntoViewIfNeeded();
await ctrl.click({ force: true });
await ctrl.fill('');
await ctrl.type(us, { delay: 15 });
}
}
test.describe('@p1 Profile — Career editor (44a)', () => {
test.setTimeout(20000);
test('Create new career from list → save → roadmap, then edit title', async ({ page }) => {
const u = loadTestUser();
// Premium gate
await page.route(
/\/api\/user-profile\?fields=.*(firstname|is_premium|is_pro_premium).*/i,
r => r.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ firstname: 'Tester', is_premium: 1, is_pro_premium: 0 }) })
);
// Stub /api/careers/search to return a real career from your dataset (Compliance Managers)
await page.route(/\/api\/careers\/search\?*/i, async route => {
const url = new URL(route.request().url());
const q = (url.searchParams.get('query') || '').toLowerCase();
const list = q.includes('compliance')
? [{ title: 'Compliance Managers', soc_code: '11-9199.02', cip_codes: ['52.0304'] }]
: [];
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(list)
});
});
// List fetch on /profile/careers (allow real backend later, so don't pin to [])
await page.route('**/api/premium/career-profile/all', route => route.fallback());
// Accept POST save (UI create flow)
let createdId = null;
await page.route('**/api/premium/career-profile', async route => {
if (route.request().method() === 'POST') {
createdId = crypto.randomUUID();
await route.fulfill({
status: 200, contentType: 'application/json',
body: JSON.stringify({ career_profile_id: createdId })
});
} else {
await route.fallback();
}
});
// Sign in
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 });
// Go to list and click + New Career Profile
await page.goto('/profile/careers', { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: '+ New Career Profile' }).click();
await page.waitForURL('**/profile/careers/new/edit', { timeout: 15000 });
await expect(page.getByRole('heading', { name: /^New Career Profile$/i }))
.toBeVisible({ timeout: 15000 });
await expect(page.locator('input[name="start_date"]').first())
.toBeVisible({ timeout: 15000 });
// Choose career (exact placeholder in new-form)
const search = page.getByPlaceholder('Start typing a career...', { exact: true });
await expect(search).toBeVisible({ timeout: 5000 });
await search.click();
const waitSearch = page.waitForResponse(r => /\/api\/careers\/search/i.test(r.url()));
await search.fill('Compliance Managers');
await waitSearch;
await page.keyboard.press('Enter'); // commit
await expect(page.locator('input[disabled]')).toHaveValue('Compliance Managers', { timeout: 3000 });
// Save → roadmap
await page.getByRole('button', { name: /^Save$/ }).click();
await expect(page).toHaveURL(/\/career-roadmap\/[a-z0-9-]+$/i, { timeout: 25000 });
// Navigate to edit that same profile and tweak title (proves list→edit wiring)
// Ensure future requests use real backend
await page.unroute('**/api/premium/career-profile/all').catch(() => {});
await page.goto('/profile/careers', { waitUntil: 'domcontentloaded' });
// wait for at least one row to appear then click its edit link
await expect.poll(async () => await page.getByRole('link', { name: /^edit$/i }).count(), { timeout: 5000 }).toBeGreaterThan(0);
await page.getByRole('link', { name: /^edit$/i }).first().click();
});
test('Edit title and save → roadmap', async ({ page, request }) => {
const u = loadTestUser();
// Stub the GET that edit page calls so fields populate deterministically
await page.route('**/api/premium/career-profile/*', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
scenario_title: 'Existing Scenario',
career_name: 'Data Analyst',
soc_code: '15-2051.00',
status: 'current',
start_date: '2025-09-01',
retirement_start_date: null,
college_enrollment_status: '',
career_goals: '',
desired_retirement_income_monthly: ''
})
});
});
// PremiumRoute gate
await page.route(
/\/api\/user-profile\?fields=.*(firstname|is_premium|is_pro_premium).*/i,
r => r.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ firstname: 'Tester', is_premium: 1, is_pro_premium: 0 }) })
);
// Sign in
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: 15_000 });
// Seed a profile via backend, then go directly to its edit page
const resp = await request.post('/api/premium/career-profile', {
data: { career_name: 'Data Analyst', status: 'planned', start_date: '2025-09-01' }
});
const { career_profile_id } = await resp.json();
await page.goto(`/profile/careers/${career_profile_id}/edit`, { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: /Edit Career Profile/i })).toBeVisible({ timeout: 15_000 });
// Title
const title = byName(page, 'scenario_title');
const prev = (await title.inputValue().catch(() => 'Scenario')) || 'Scenario';
await title.fill(prev + ' — test');
// Start date (supports type=date or text mm/dd/yyyy)
const start = byName(page, 'start_date');
if ((await start.count()) > 0) {
await fillDateField(start, '2025-09-01', '09/01/2025');
}
await page.getByRole('button', { name: /^Save$/ }).click();
await expect(page).toHaveURL(/\/career-roadmap\/[a-z0-9-]+$/i, { timeout: 25_000 });
});
});

View File

@ -0,0 +1,82 @@
// tests/e2e/45b-financial-profile.spec.mjs
// @ts-check
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
const byName = (page, n) => page.locator(`input[name="${n}"]`).first();
// Label → first following input (works even without for/id)
const inputAfterLabel = (page, labelRe) =>
page.locator('label').filter({ hasText: labelRe }).first()
.locator('xpath=following::input[1]').first();
test.describe('@p1 Profile — Financial editor (45b)', () => {
test.setTimeout(45000);
test('Save financial profile (accept alert) → leaves page', async ({ page }) => {
const u = loadTestUser();
// Premium gate
await page.route(
/\/api\/user-profile\?fields=.*(firstname|is_premium|is_pro_premium).*/i,
r => r.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ firstname: 'Tester', is_premium: 1, is_pro_premium: 0 })
})
);
// Deterministic GET/POST for the form
await page.route('**/api/premium/financial-profile', async route => {
if (route.request().method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
current_salary: 0,
additional_income: 0,
monthly_expenses: 0,
monthly_debt_payments: 0,
retirement_savings: 0,
emergency_fund: 0,
retirement_contribution: 0,
emergency_contribution: 0,
extra_cash_emergency_pct: 50,
extra_cash_retirement_pct: 50
})
});
} else if (route.request().method() === 'POST') {
await route.fulfill({ status: 200, body: '{}' });
} else {
await route.fallback();
}
});
// Sign in
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: 15_000 });
// Go to form
await page.goto('/financial-profile', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: /Edit\s*Your\s*Financial\s*Profile/i }))
.toBeVisible({ timeout: 15_000 });
// Fill minimal fields
await byName(page, 'currentSalary').fill('55000');
// This field has NO name attribute → select by label then first input
await inputAfterLabel(page, /Monthly\s*Living\s*Expenses/i).fill('1800');
await byName(page, 'monthlyDebtPayments').fill('200');
await byName(page, 'extraCashEmergencyPct').fill('40'); // complements to 60
// Accept expected alert then leave page
page.once('dialog', d => d.accept().catch(() => {}));
await page.getByRole('button', { name: /^Save$/ }).click();
await expect(page).not.toHaveURL(/\/financial-profile$/i, { timeout: 20_000 });
});
});

View File

@ -0,0 +1,262 @@
// @ts-check
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
const SCHOOL = 'Alabama A & M University';
const PROGRAM = 'Agriculture, General.';
const DEGREE = "Bachelor's Degree";
async function fillDateField(ctrl, iso = '2027-06-01', us = '06/01/2027') {
const type = (await ctrl.getAttribute('type')) || '';
if (type.toLowerCase() === 'date') {
await ctrl.fill(iso);
} else {
await ctrl.scrollIntoViewIfNeeded();
await ctrl.click({ force: true });
await ctrl.fill('');
await ctrl.type(us, { delay: 15 });
}
}
async function commitAutosuggest(page, input, text) {
await input.click();
await input.fill(text);
await page.keyboard.press('ArrowDown').catch(() => {});
await page.keyboard.press('Enter').catch(() => {});
await input.blur();
}
test.describe('@p1 College-Profile (45c)', () => {
test.setTimeout(20000);
test('Create new college plan from list (with link-to-career prompt) → save', async ({ page, request }) => {
const u = loadTestUser();
// Premium gate
await page.route(
/\/api\/user-profile\?fields=.*(firstname|is_premium|is_pro_premium).*/i,
r => r.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ firstname: 'Tester', is_premium: 1, is_pro_premium: 0 }) })
);
// Seed a minimal Career Profile (needed for the prompt dropdown)
const scen = await request.post('/api/premium/career-profile', {
data: { career_name: 'Teaching Assistants, Postsecondary', status: 'planned', start_date: '2025-09-01' }
});
const { career_profile_id } = await scen.json();
// Deterministic autosuggest + degree + tuition
await page.route(/\/api\/schools\/suggest\?*/i, async route => {
await route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify([{ name: 'Alabama A & M University', unitId: 100654 }]) });
});
await page.route(/\/api\/programs\/suggest\?*/i, async route => {
await route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify([{ program: 'Agriculture, General.' }]) });
});
await page.route(/\/api\/programs\/types\?*/i, async route => {
await route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ types: ["Bachelor's Degree"] }) });
});
await page.route(/\/api\/tuition\/estimate\?*/i, async route => {
await route.fulfill({ status: 200, contentType: 'application/json',
body: JSON.stringify({ estimate: 17220 }) });
});
// Accept POST save
await page.route('**/api/premium/college-profile', async route => {
if (route.request().method() === 'POST') {
await route.fulfill({ status: 200, body: JSON.stringify({ ok: true }) });
} else {
await route.fallback();
}
});
// Sign in
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 });
// List → + New College Plan (expect link-to-career prompt)
await page.goto('/profile/college', { waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: '+ New College Plan' }).click();
// The prompt is a labeled box; target the <select> inside that label container.
const promptBox = page.locator('text=Select the career this college plan belongs to:').first();
await expect(promptBox).toBeVisible({ timeout: 5000 });
const promptSelect = promptBox.locator('..').locator('select').first();
await expect(promptSelect).toBeVisible({ timeout: 5000 });
await promptSelect.selectOption(String(career_profile_id));
// Selecting navigates to new editor with query params (?career=…)
await expect(page).toHaveURL(/\/profile\/college\/new\?career=.*$/i, { timeout: 10000 });
await expect(page.getByRole('heading', { name: /College Plan|New College Plan/i }))
.toBeVisible({ timeout: 10000 });
// Fill required fields on editor
const school = page.locator('input[name="selected_school"]').first();
await school.fill('Alabama');
await page.keyboard.press('ArrowDown').catch(() => {});
await page.keyboard.press('Enter').catch(() => {});
const program = page.locator('input[name="selected_program"]').first();
await program.fill('Agri');
await page.keyboard.press('ArrowDown').catch(() => {});
await page.keyboard.press('Enter').catch(() => {});
await page.locator('select[name="program_type"]').first().selectOption({ label: "Bachelor's Degree" }).catch(() => {});
const grad = page.locator('input[name="expected_graduation"]').first();
const t = (await grad.getAttribute('type')) || '';
if (t.toLowerCase() === 'date') await grad.fill('2028-05-01');
else { await grad.fill(''); await grad.type('05/01/2028', { delay: 15 }); }
page.once('dialog', d => d.accept().catch(() => {}));
await page.getByRole('button', { name: /^Save$/ }).click();
await expect(page).not.toHaveURL(/\/edit$/i, { timeout: 10000 });
});
test('Edit existing plan: autosuggest + degree + save', async ({ page, request }) => {
const u = loadTestUser();
// Premium gate
await page.route(
/\/api\/user-profile\?fields=.*(firstname|is_premium|is_pro_premium).*/i,
r => r.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ firstname: 'Tester', is_premium: 1, is_pro_premium: 0 })
})
);
// Deterministic autosuggests/types/tuition
await page.route(/\/api\/schools\/suggest\?*/i, async route => {
const url = new URL(route.request().url());
const q = (url.searchParams.get('query') || '').toLowerCase();
const list = q.includes('alabama') ? [{ name: SCHOOL, unitId: 100654 }] : [];
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(list) });
});
await page.route(/\/api\/programs\/suggest\?*/i, async route => {
const url = new URL(route.request().url());
const school = (url.searchParams.get('school') || '').toLowerCase();
const q = (url.searchParams.get('query') || '').toLowerCase();
const list = school.includes('alabama a & m') && (q.includes('agri') || q === '')
? [{ program: PROGRAM }]
: [];
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(list) });
});
await page.route(/\/api\/programs\/types\?*/i, async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ types: [DEGREE, "Associate's Degree", "Master's Degree"] })
});
});
await page.route(/\/api\/tuition\/estimate\?*/i, async route => {
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ estimate: 17220 }) });
});
// Initial GET for edit page
await page.route(/\/api\/premium\/college-profile\?careerProfileId=.*/i, async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
selected_school: SCHOOL,
selected_program: PROGRAM,
program_type: DEGREE,
expected_graduation: '2027-06-01',
academic_calendar: 'semester',
credit_hours_per_year: 27,
interest_rate: 5.5,
loan_term: 10,
tuition: 17220,
unit_id: 100654,
loan_deferral_until_graduation: 1
})
});
});
// Accept POST save
await page.route('**/api/premium/college-profile', async route => {
if (route.request().method() === 'POST') {
await route.fulfill({ status: 200, body: '{}' });
} else {
await route.fallback();
}
});
// Sign in
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 });
// Create scenario + minimal plan server-side
const scen = await request.post('/api/premium/career-profile', {
data: { career_name: 'QA College Plan', status: 'planned', start_date: '2025-09-01' }
});
const { career_profile_id } = await scen.json();
await request.post('/api/premium/college-profile', {
data: {
career_profile_id,
selected_school: SCHOOL,
selected_program: PROGRAM,
program_type: DEGREE,
expected_graduation: '2027-06-01',
academic_calendar: 'semester',
credit_hours_per_year: 27,
interest_rate: 5.5,
loan_term: 10
}
});
// Go to edit
await page.goto(`/profile/college/${career_profile_id}/edit`, { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: /Edit College Plan|College Plan/i })).toBeVisible({ timeout: 20000 });
// School (partial -> suggestions -> commit exact)
const schoolBox = page.locator('input[name="selected_school"]').first();
await schoolBox.fill('Alabama A &');
await expect.poll(async () => await page.locator('#school-suggestions option').count(), { timeout: 5000 })
.toBeGreaterThan(0);
await commitAutosuggest(page, schoolBox, SCHOOL);
// Program (partial -> suggestions -> commit exact)
const programBox = page.locator('input[name="selected_program"]').first();
await programBox.fill('Agri');
await expect.poll(async () => await page.locator('#program-suggestions option').count(), { timeout: 5000 })
.toBeGreaterThan(0);
await commitAutosuggest(page, programBox, PROGRAM);
// Degree
const degreeSelect = page.locator('select[name="program_type"]').first();
await expect.poll(async () => await degreeSelect.locator('option').count(), { timeout: 15000 }).toBeGreaterThan(1);
try {
await degreeSelect.selectOption({ label: DEGREE });
} catch {
const firstReal = degreeSelect.locator('option').nth(1);
const val = await firstReal.getAttribute('value');
if (val) await degreeSelect.selectOption(val);
}
// Expected Graduation Date
const grad = page.locator('input[name="expected_graduation"]').first();
await fillDateField(grad, '2028-05-01', '05/01/2028');
// Save (accept alert) and ensure we left /edit
page.once('dialog', d => d.accept().catch(() => {}));
await page.getByRole('button', { name: /^Save$/i }).click();
await expect(page).not.toHaveURL(/\/edit$/i, { timeout: 10000 });
// Sanity: no inline validation error text rendered
await expect(page.getByText(/Please pick a (school|program) from the list/i)).toHaveCount(0, { timeout: 2000 })
.catch(() => {});
});
});

View File

@ -1,239 +0,0 @@
// @ts-check
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p1 Profile Editors — Career, Financial, and College', () => {
test.setTimeout(90_000);
test.beforeEach(async ({ page }) => {
const u = loadTestUser();
// Premium gate that App.js / PremiumRoute actually uses
await page.route(
/\/api\/user-profile\?fields=.*(firstname|is_premium|is_pro_premium).*/i,
async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ firstname: 'Tester', is_premium: 1, is_pro_premium: 0 })
});
}
);
// Real sign-in (single session per test)
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: 15_000 });
});
// -------- Career profile (edit) --------
test('CareerProfileList → edit first profile title and save', async ({ page }) => {
// Ensure at least one
await page.request.post('/api/premium/career-profile', {
data: { career_name: 'QA Scenario', status: 'planned', start_date: '2025-09-01' }
});
await page.goto('/profile/careers', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: 'Career Profiles' })).toBeVisible();
const editLinks = page.locator('a', { hasText: /^edit$/i });
await editLinks.first().click();
await expect(page.getByRole('heading', { name: /Edit Career Profile|New Career Profile/i }))
.toBeVisible({ timeout: 15_000 });
const title = page.getByLabel(/Scenario Title/i);
const prev = await title.inputValue().catch(() => 'Scenario');
await title.fill((prev || 'Scenario') + ' — test');
await page.getByRole('button', { name: /^Save$/ }).click();
await expect(page).toHaveURL(/\/career-roadmap\/[a-z0-9-]+$/i, { timeout: 20_000 });
});
// -------- Financial profile (save) --------
test('FinancialProfileForm saves and returns', async ({ page }) => {
await page.goto('/profile/financial', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: /Financial Profile/i })).toBeVisible({ timeout: 10_000 });
await page.getByLabel(/Current.*Annual.*Salary/i).fill('55000');
await page.getByLabel(/Monthly.*Living.*Expenses/i).fill('1800');
await page.getByLabel(/Monthly.*Debt.*Payments/i).fill('200');
await page.getByLabel(/To.*Emergency.*\(%\)/i).fill('40'); // 60 to retirement auto-computed
await page.getByRole('button', { name: /^Save$/ }).click();
await expect(page).not.toHaveURL(/\/profile\/financial$/i, { timeout: 15_000 });
});
// Helper: commit autosuggest that sometimes needs two selects
async function commitAutosuggest(page, input, text) {
await input.click();
await input.fill(text);
await page.keyboard.press('ArrowDown').catch(() => {});
await page.keyboard.press('Enter').catch(() => {});
await input.blur();
const v1 = (await input.inputValue()).trim();
if (v1.toLowerCase() === text.toLowerCase()) {
// Some nested lists require a second confirmation
await input.click();
await page.keyboard.press('ArrowDown').catch(() => {});
await page.keyboard.press('Enter').catch(() => {});
await input.blur();
}
}
// ---- College helpers (put near the top of the file) ----
async function commitAutosuggest(page, input, text) {
// Type and try first commit
await input.click();
await input.fill(text);
await page.keyboard.press('ArrowDown').catch(() => {});
await page.keyboard.press('Enter').catch(() => {});
await input.blur();
// If typed value still equals exactly what we wrote, some lists need a second confirm
const v1 = (await input.inputValue()).trim();
if (v1.toLowerCase() === text.toLowerCase()) {
await input.click();
await page.keyboard.press('ArrowDown').catch(() => {});
await page.keyboard.press('Enter').catch(() => {});
await input.blur();
}
}
async function pickDegree(page) {
const select = page.getByLabel(/Degree Type/i);
try {
await select.selectOption({ label: "Bachelor's Degree" });
return;
} catch {}
const second = select.locator('option').nth(1); // skip placeholder
await second.waitFor({ state: 'attached', timeout: 10_000 });
const val = await second.getAttribute('value');
if (val) await select.selectOption(val);
}
// Known-good school/program pair from your dataset
const SCHOOL = 'Alabama A & M University';
const PROGRAM = 'Agriculture, General.';
// ---- CollegeProfileForm — create NEW plan (autosuggests must appear, then save) ----
test('CollegeProfileForm — create new plan (autosuggests + degree + save)', async ({ page, request }) => {
// Fail the test if any unexpected alert shows
page.on('dialog', d => {
throw new Error(`Unexpected dialog: "${d.message()}"`);
});
// Create a scenario to attach the college plan
const scen = await request.post('/api/premium/career-profile', {
data: { career_name: 'QA College Plan (new)', status: 'planned', start_date: '2025-09-01' }
});
const { career_profile_id } = await scen.json();
await page.goto(`/profile/college/${career_profile_id}/new`, { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: /College Plan|Edit College Plan|New College/i }))
.toBeVisible({ timeout: 15_000 });
const SCHOOL = 'Alabama A & M University';
const PROGRAM = 'Agriculture, General.';
const schoolBox = page.getByLabel(/School Name/i);
const programBox = page.getByLabel(/Major.*Program.*Name/i);
// Type partial and assert autosuggest options exist (prove the dropdown is presented)
await schoolBox.fill('Alabama A &');
await expect.poll(async () => {
return await page.locator('#school-suggestions option').count();
}, { timeout: 5000 }).toBeGreaterThan(0);
// Commit school programmatically with double-confirm helper
await commitAutosuggest(page, schoolBox, SCHOOL);
// Program autosuggest must be present too
await programBox.fill('Agri');
await expect.poll(async () => {
return await page.locator('#school-suggestions option').count();
}, { timeout: 5000 }).toBeGreaterThan(0);
// Commit program
await commitAutosuggest(page, programBox, PROGRAM);
// Pick degree + set expected graduation
await pickDegree(page);
const grad = page.getByLabel(/Expected Graduation Date/i);
if (await grad.isVisible().catch(() => false)) await grad.fill('2027-06-01');
// Save
await page.getByRole('button', { name: /^Save$/i }).click();
// Should not remain stuck on /new (and no alerts were raised)
await expect(page).not.toHaveURL(/\/new$/i, { timeout: 10_000 });
});
// ---- CollegeProfileForm — EDIT existing plan (autosuggests + degree + save) ----
test('CollegeProfileForm — edit existing plan (autosuggests + degree + save)', async ({ page, request }) => {
// Fail the test if any unexpected alert shows
page.on('dialog', d => {
throw new Error(`Unexpected dialog: "${d.message()}"`);
});
// Create a scenario and seed a minimal college plan
const scen = await request.post('/api/premium/career-profile', {
data: { career_name: 'QA College Plan (edit)', status: 'planned', start_date: '2025-09-01' }
});
const { career_profile_id } = await scen.json();
const SCHOOL = 'Alabama A & M University';
const PROGRAM = 'Agriculture, General.';
await request.post('/api/premium/college-profile', {
data: {
career_profile_id,
selected_school: SCHOOL,
selected_program: PROGRAM,
program_type: "Bachelor's Degree",
expected_graduation: '2027-06-01',
academic_calendar: 'semester',
credit_hours_per_year: 30,
interest_rate: 5.5,
loan_term: 10
}
});
await page.goto(`/profile/college/${career_profile_id}/edit`, { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: /College Plan|Edit College Plan/i }))
.toBeVisible({ timeout: 15_000 });
const schoolBox = page.getByLabel(/School Name/i);
const programBox = page.getByLabel(/Major.*Program.*Name/i);
// Assert autosuggest presents options on partial typing
await schoolBox.fill('Alabama A &');
await expect.poll(async () => {
return await page.locator('#school-suggestions option').count();
}, { timeout: 5000 }).toBeGreaterThan(0);
// Recommit the same school/program (exercise autosuggest again)
await commitAutosuggest(page, schoolBox, SCHOOL);
await programBox.fill('Agri');
await expect.poll(async () => {
return await page.locator('#school-suggestions option').count();
}, { timeout: 5000 }).toBeGreaterThan(0);
await commitAutosuggest(page, programBox, PROGRAM);
await pickDegree(page);
const grad = page.getByLabel(/Expected Graduation Date/i);
if (await grad.isVisible().catch(() => false)) await grad.fill('2028-05-01');
await page.getByRole('button', { name: /^Save$/i }).click();
// No error popup was allowed; we just assert were not showing the error text
await expect(page.getByText(/Please pick a school from the list/i))
.toHaveCount(0, { timeout: 2000 })
.catch(() => {});
});
});

View File

@ -2,34 +2,171 @@
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p1 CareerRoadmap — panels + Coach basics', () => {
test.setTimeout(60000);
const j = (o) => JSON.stringify(o);
test('open roadmap for a known scenario; coach replies', async ({ page }) => {
test.describe('@p1 CareerRoadmap — core panels + coach (47)', () => {
test.setTimeout(20000);
test('Loads coach UI, salary/econ panels, and projection chart', async ({ page, request }) => {
const u = loadTestUser();
await page.route('**/api/premium/subscription/status', async route => {
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ is_premium: 1, is_pro_premium: 0 }) });
});
// ── Premium gate (include area/state for salary/econ)
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 (real form)
await page.context().clearCookies();
await page.goto('/signin', { waitUntil: 'networkidle' });
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 });
const scen = await page.request.post('/api/premium/career-profile', {
data: { career_name: 'QA Roadmap', status: 'planned', start_date: '2025-09-01' }
// ── Seed a scenario; capture id
const scen = await request.post('/api/premium/career-profile', {
data: { career_name: 'Software Developers', status: 'planned', start_date: '2024-01-01' }
});
const { career_profile_id } = await scen.json();
await page.goto(`/career-roadmap/${career_profile_id}`, { waitUntil: 'networkidle' });
await expect(page.getByText(/Where you are now and where you are going/i)).toBeVisible({ timeout: 20000 });
// ── Deterministic reads CareerRoadmap expects
await page.route(/\/api\/premium\/financial-profile$/i, r => r.fulfill({
status: 200, contentType: 'application/json',
body: j({
current_salary: 85000,
additional_income: 0,
monthly_expenses: 2500,
monthly_debt_payments: 300,
retirement_savings: 12000,
emergency_fund: 4000,
retirement_contribution: 300,
emergency_contribution: 200,
extra_cash_emergency_pct: 50,
extra_cash_retirement_pct: 50
})
}));
await expect(page.getByRole('heading', { name: 'Career Coach' })).toBeVisible();
await page.getByPlaceholder('Ask your Career Coach…').fill('Give me one tip.');
await page.getByRole('button', { name: /^Send$/ }).click();
await expect(page.getByText(/Coach is typing…/i)).toBeVisible();
await expect(page.getByText(/Coach is typing…/i)).toHaveCount(0, { timeout: 25000 });
// Scenario row: include soc_code to skip /api/careers/resolve
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: '2024-01-01',
college_enrollment_status: 'not_enrolled'
})
}));
// College profile — return explicit values to guarantee projection data
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: 8000,
interest_rate: 5.5,
loan_term: 10,
loan_deferral_until_graduation: 0,
academic_calendar: 'monthly',
annual_financial_aid: 0,
tuition: 0,
extra_payment: 0,
expected_salary: 90000
})
})
);
// Salary (regional + national)
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 }
})
}));
// Economic projections (state + national)
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'
}
})
}));
// ── Navigate to roadmap
await page.goto(`/career-roadmap/${career_profile_id}`, { waitUntil: 'domcontentloaded' });
// 1) Coach UI mounted (intro text is async; dont assert it)
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 });
const sendBtn = page.getByRole('button', { name: /^Send$/ });
await expect(sendBtn).toBeVisible({ timeout: 6000 });
const coachInput = sendBtn.locator('..').getByRole('textbox').first();
await expect(coachInput).toBeVisible({ timeout: 6000 });
// 2) Context section + target career
await expect(page.getByRole('heading', { name: /Where you are now and where you are going/i }))
.toBeVisible({ timeout: 6000 });
await expect(page.getByText(/Target Career:\s*Software Developers/i)).toBeVisible({ timeout: 6000 });
// 3) Salary panels — scope to each card to avoid duplicate “Median $…”
const regionalHeader = page.getByText(/Regional Salary Data/i).first();
await expect(regionalHeader).toBeVisible({ timeout: 6000 });
const regionalCard = regionalHeader.locator('..');
await expect(regionalCard.getByText(/Median:\s*\$100,000/i)).toBeVisible({ timeout: 6000 });
const nationalHeader = page.getByText(/National Salary Data/i).first();
await expect(nationalHeader).toBeVisible({ timeout: 6000 });
const nationalCard = nationalHeader.locator('..');
await expect(nationalCard.getByText(/Median:\s*\$98,000/i)).toBeVisible({ timeout: 6000 });
// 4) Economic projections — two bars visible
await expect(page.getByText(/^Georgia$/)).toBeVisible({ timeout: 6000 });
await expect(page.getByText(/^United States$/)).toBeVisible({ timeout: 6000 });
// 5) Financial Projection — header + the charts canvas in that section
const fpHeader = page.getByRole('heading', { name: /Financial Projection/i }).first();
await fpHeader.scrollIntoViewIfNeeded(); // ensure below-the-fold section is in view
await expect(fpHeader).toBeVisible({ timeout: 6000 });
// Nearest card container → canvas inside (ChartJS)
const fpCard = fpHeader.locator('xpath=ancestor::div[contains(@class,"bg-white")][1]');
// Give a moment for projection build after college-profile resolves
await expect.poll(async () => await fpCard.locator('canvas').count(), { timeout: 9000 }).toBeGreaterThan(0);
await expect(fpCard.locator('canvas').first()).toBeVisible({ timeout: 6000 });
// 6) Milestones header visible (scroll + relaxed name to allow tooltip text)
const msHeader = page.getByRole('heading', { name: /Milestones/i }).first();
await msHeader.scrollIntoViewIfNeeded();
await expect(msHeader).toBeVisible({ timeout: 6000 });
});
});

View File

@ -2,67 +2,382 @@
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p1 Milestones — create (modal), edit, delete', () => {
test.setTimeout(60000);
const j = (o) => JSON.stringify(o);
test('ensure scenario, open Milestones, add + edit + delete', async ({ page }) => {
/** 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();
await page.route('**/api/premium/subscription/status', async route => {
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ is_premium: 1, is_pro_premium: 0 }) });
});
// ── 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: 'networkidle' });
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 });
const scen = await page.request.post('/api/premium/career-profile', {
data: { career_name: 'QA Milestones', status: 'planned', start_date: '2025-09-01' }
// ── 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();
await page.goto(`/career-roadmap/${career_profile_id}`, { waitUntil: 'networkidle' });
// Milestones section anchor
await expect(page.getByRole('heading', { name: /^Milestones$/ })).toBeVisible({ timeout: 20000 });
// ── 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'
})
})
);
// Open MilestoneEditModal via "Add Details" (missing banner) or panel button
const addDetails = page.getByRole('button', { name: /Add Details/i });
if (await addDetails.isVisible().catch(() => false)) {
await addDetails.click();
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<number, Array<any>>} */
let tasksByMilestone = {
[existingMilestoneId]: [
{ id: existingTaskId, title: 'Draft self-review', description: 'Write draft', due_date: '2026-03-15', status: 'not_started' }
],
[createdMilestoneId]: []
};
/** @type {Record<number, Array<any>>} */
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 <summary>Add new milestone</summary> 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 {
// Fallback: open the modal through any visible “Milestones” action button
const anyBtn = page.getByRole('button', { name: /Add|Edit|Milestone/i }).first();
await anyBtn.click().catch(() => {});
await createDate.fill('');
await createDate.type('05/01/2026', { delay: 10 });
}
// Modal header
await expect(page.getByRole('heading', { name: /^Milestones$/ })).toBeVisible({ timeout: 15000 });
await newSection.locator('label:has-text("Description")').locator('..').locator('textarea').first()
.fill('Ship v1 and announce.');
// Add new milestone section
await page.getByText(/Add new milestone/i).click();
await page.getByLabel(/^Title$/i).first().fill('QA Milestone A');
await page.getByLabel(/^Date$/i).first().fill('2026-04-01');
await page.getByRole('button', { name: /\+ Add impact/i }).click();
await page.getByLabel(/^Type$/i).first().selectOption('ONE_TIME');
await page.getByLabel(/^Amount$/i).first().fill('123');
await page.getByLabel(/^Start$/i).first().fill('2026-03-01');
await page.getByRole('button', { name: /\+ Add task/i }).click();
await page.getByLabel(/^Title$/i).nth(1).fill('Prepare docs');
await page.getByLabel(/^Due Date$/i).first().fill('2026-03-15');
// 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');
await page.getByRole('button', { name: /Save milestone/i }).click();
await expect(page.getByText(/QA Milestone A/i)).toBeVisible({ timeout: 20000 });
// 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');
// Edit existing milestone: open accordion by its title
await page.getByRole('button', { name: /QA Milestone A/i }).click();
await page.getByLabel(/Description/i).fill('Short description');
await page.getByRole('button', { name: /^Save$/ }).click();
// Save
await modalRoot.getByRole('button', { name: /^Save milestone$/i }).click();
// Delete milestone
await page.getByRole('button', { name: /Delete milestone/i }).click();
// Close modal
await page.getByRole('button', { name: /^Close$/ }).click();
// Ensure listed in panel and visible — expand the exact <details> 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 <details> 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 });
});
});

View File

@ -0,0 +1,157 @@
// @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 });
});
});

View File

@ -1,37 +0,0 @@
// @ts-check
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p2 Coach — quick actions', () => {
test.setTimeout(60000);
test('Networking Plan triggers and yields reply without leaking hidden prompts', async ({ page }) => {
const u = loadTestUser();
await page.route('**/api/premium/subscription/status', async route => {
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ is_premium: 1, is_pro_premium: 0 }) });
});
await page.context().clearCookies();
await page.goto('/signin', { waitUntil: 'networkidle' });
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 });
const scen = await page.request.post('/api/premium/career-profile', {
data: { career_name: 'QA Coach', status: 'planned', start_date: '2025-09-01' }
});
const { career_profile_id } = await scen.json();
await page.goto(`/career-roadmap/${career_profile_id}`, { waitUntil: 'networkidle' });
await expect(page.getByRole('heading', { name: 'Career Coach' })).toBeVisible({ timeout: 20000 });
await page.getByRole('button', { name: /Networking Plan/i }).click();
await expect(page.getByText(/Coach is typing/i)).toBeVisible();
await expect(page.getByText(/Coach is typing/i)).toHaveCount(0, { timeout: 30000 });
// Hidden system prompts must not leak
const leaks = await page.locator('text=/^# ⛔️|^MODE :|\"milestones\"/').count();
expect(leaks).toBe(0);
});
});

View File

@ -0,0 +1,103 @@
// @ts-check
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
const j = (o) => JSON.stringify(o);
/** Try common routes until the heading "Resume Optimizer" appears. */
async function gotoResume(page) {
const candidates = [
'/resume-optimizer',
'/tools/resume',
'/premium/resume',
'/resume'
];
for (const path of candidates) {
await page.goto(path, { waitUntil: 'domcontentloaded' }).catch(() => {});
const heading = page.getByRole('heading', { name: /^Resume Optimizer$/i }).first();
if (await heading.count()) return true;
}
return false;
}
test.describe('@p1 ResumeRewrite — upload + optimize (50)', () => {
test.setTimeout(20000);
test('Uploads PDF, fills fields, sees spinner, and shows optimized resume', async ({ page }) => {
const u = loadTestUser();
// Premium/user gate (keeps app happy if it checks)
await page.route(
/\/api\/user-profile\?fields=.*(firstname|is_premium|is_pro_premium).*/i,
r => r.fulfill({
status: 200,
contentType: 'application/json',
body: j({ firstname: 'Tester', is_premium: 1, is_pro_premium: 0 })
})
);
// Remaining optimizations banner
await page.route(/\/api\/premium\/resume\/remaining$/i, r => r.fulfill({
status: 200,
contentType: 'application/json',
body: j({ remainingOptimizations: 3, resetDate: '2026-01-31T00:00:00Z' })
}));
// Optimize endpoint — simulate a short delay and return deterministic text
await page.route(/\/api\/premium\/resume\/optimize$/i, async r => {
await new Promise(res => setTimeout(res, 350)); // small “thinking” delay
return r.fulfill({
status: 200,
contentType: 'application/json',
body: j({
optimizedResume:
`John Q. Dev
Optimized for Software Engineer
Quantified accomplishments aligned to the job description.
Highlighted skills: JavaScript, React, APIs.
Keyword-tuned for ATS while keeping clarity.`
})
});
});
// Sign in
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();
// Find the Resume Optimizer page (try common routes)
const found = await gotoResume(page);
expect(found).toBeTruthy();
// Banner shows remaining count
await expect(page.getByText(/3.*Resume Optimizations Remaining/i)).toBeVisible({ timeout: 4000 });
await expect(page.getByText(/Resets on/i)).toBeVisible({ timeout: 4000 });
// Upload a tiny in-memory PDF (valid mime & small size)
const pdfBytes = Buffer.from('%PDF-1.4\n%âãÏÓ\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF', 'utf-8');
await page.locator('input[type="file"]').setInputFiles({
name: 'resume.pdf',
mimeType: 'application/pdf',
buffer: pdfBytes
});
// Fill Job Title and Job Description
await page.getByPlaceholder(/e\.g\.,?\s*Software Engineer/i).fill('Software Engineer');
await page.getByPlaceholder(/Paste the job listing|requirements here/i)
.fill('We need a JavaScript/React engineer to build customer-facing features.');
// Submit
const submitBtn = page.getByRole('button', { name: /^Optimize Resume$/i });
await submitBtn.click();
// Spinner + disabled button during optimization
await expect(page.getByText(/Optimizing your resume/i)).toBeVisible({ timeout: 2000 });
// Optimized Resume appears, and button re-enables
await expect(page.getByRole('heading', { name: /^Optimized Resume$/i })).toBeVisible({ timeout: 7000 });
await expect(page.locator('pre')).toContainText(/Optimized for Software Engineer/i, { timeout: 7000 });
});
});

View File

@ -0,0 +1,155 @@
// @ts-check
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
const j = (o) => JSON.stringify(o);
test.describe('@p1 ScenarioEditModal — save + retirement milestone (51)', () => {
test.setTimeout(20000);
test('Open modal, set retirement fields, save → creates Retirement 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: 'U.S.', state: 'GA' })
})
);
// ── Seed a scenario
const seed = await request.post('/api/premium/career-profile', {
data: { career_name: 'Software Developers', status: 'planned', start_date: '2025-01-01' }
});
const { career_profile_id } = await seed.json();
// ── CareerRoadmap deps (minimal)
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,
scenario_title: 'SWE Plan',
career_name: 'Software Developers',
status: 'planned',
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({}) })
);
// Salary/Projections to keep UI calm
await page.route(/\/api\/salary\?*/i, r => r.fulfill({ status: 200, contentType: 'application/json', body: j({}) }));
await page.route(/\/api\/projections\/15-1252\?state=.*/i, r => r.fulfill({ status: 200, contentType: 'application/json', body: j({}) }));
// Milestones list for CareerRoadmap panel (empty)
await page.route(new RegExp(`/api/premium/milestones\\?careerProfileId=${career_profile_id}$`, 'i'), r =>
r.fulfill({ status: 200, contentType: 'application/json', body: j({ milestones: [] }) })
);
// POST /career-profile (upsert on save)
await page.route(/\/api\/premium\/career-profile$/i, async r => {
if (r.request().method() === 'POST') {
return r.fulfill({
status: 200, contentType: 'application/json',
body: j({ career_profile_id })
});
}
return r.fallback();
});
// GET milestones?careerProfileId=.. (check Retirement exists) → none
await page.route(/\/api\/premium\/milestones\?careerProfileId=\d+$/i, r => {
return r.fulfill({
status: 200, contentType: 'application/json',
body: j({ milestones: [] }) // no Retirement yet → should POST
});
});
// POST /milestone (create Retirement)
await page.route(/\/api\/premium\/milestone$/i, async r => {
if (r.request().method() === 'POST') {
return r.fulfill({ status: 200, contentType: 'application/json', body: j({ id: 70001 }) });
}
return r.fallback();
});
// PUT /milestones/:id (would be used if it already existed)
await page.route(/\/api\/premium\/milestones\/\d+$/i, async r => {
if (r.request().method() === 'PUT') {
return r.fulfill({ status: 200, body: '{}' });
}
return r.fallback();
});
// ── Navigate to roadmap
await page.goto(`/career-roadmap/${career_profile_id}`, { waitUntil: 'domcontentloaded' });
// Open the ScenarioEditModal
const editBtn = page.getByRole('button', { name: /^Edit Simulation Inputs$/i });
await expect(editBtn).toBeVisible({ timeout: 6000 });
await editBtn.click();
// Modal visible
const modalHeading = page.getByRole('heading', { name: /^Edit Scenario:/i }).first();
await expect(modalHeading).toBeVisible({ timeout: 6000 });
// Set retirement date and desired monthly income (robust selectors)
const retireField = page.locator('input[name="retirement_start_date"]').first();
const dtype = (await retireField.getAttribute('type')) || '';
if (dtype.toLowerCase() === 'date') {
await retireField.fill('2035-06-01');
} else {
await retireField.fill('');
await retireField.type('06/01/2035', { delay: 10 });
}
const incomeField = page.locator('input[name="desired_retirement_income_monthly"]').first();
await incomeField.fill('5500');
// Prepare network waits before clicking Save
const waitUpsert = page.waitForRequest(req =>
/\/api\/premium\/career-profile$/i.test(req.url()) && req.method() === 'POST'
);
const waitMilestonesCheck = page.waitForRequest(req =>
new RegExp(`/api/premium/milestones\\?careerProfileId=${career_profile_id}$`, 'i').test(req.url())
);
const waitRetirementCreateOrPut = page.waitForRequest(req =>
/\/api\/premium\/milestone($|\/\d+$)/i.test(req.url()) && /^(POST|PUT)$/.test(req.method())
);
// Save
await page.getByRole('button', { name: /^Save$/i }).click();
// Modal closes
await expect(modalHeading).toHaveCount(0, { timeout: 6000 });
// Verify requests fired
await Promise.all([
waitUpsert,
waitMilestonesCheck,
waitRetirementCreateOrPut
]);
});
});