Added Premium Tests - fixed CollegeProfileForm.js
@ -1 +1 @@
|
||||
24c4644c626acf48ddca3964105cd9bfa267d82a-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
720d57c0d6d787c629f53d47689088a50b9e9b5b-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
|
||||
@ -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'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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 can’t accommodate every job title—choose the closest match to what you’re 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]
|
||||
```
|
||||
|
After Width: | Height: | Size: 98 KiB |
@ -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 can’t accommodate every job title—choose the closest match to what you’re 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]
|
||||
```
|
||||
|
After Width: | Height: | Size: 98 KiB |
@ -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 can’t accommodate every job title—choose the closest match to what you’re searching for.
|
||||
- paragraph [ref=e33]: After you pick a career, we’ll display matching educational programs.
|
||||
- button "Open chat" [ref=e34] [cursor=pointer]:
|
||||
- img [ref=e35] [cursor=pointer]
|
||||
```
|
||||
|
After Width: | Height: | Size: 55 KiB |
@ -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 can’t accommodate every job title—choose the closest match to what you’re searching for.
|
||||
- paragraph [ref=e33]: After you pick a career, we’ll display matching educational programs.
|
||||
- button "Open chat" [ref=e34] [cursor=pointer]:
|
||||
- img [ref=e35] [cursor=pointer]
|
||||
```
|
||||
|
After Width: | Height: | Size: 55 KiB |
@ -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 can’t accommodate every job title—choose the closest match to what you’re 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]
|
||||
```
|
||||
|
After Width: | Height: | Size: 91 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 28 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 26 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 26 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 28 KiB |
@ -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
|
||||
```
|
||||
|
Before Width: | Height: | Size: 33 KiB |
@ -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
|
||||
```
|
||||
|
Before Width: | Height: | Size: 33 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 28 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 28 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 28 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 28 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 28 KiB |
@ -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: Don’t have an account?
|
||||
- link "Sign Up" [ref=e17] [cursor=pointer]:
|
||||
- /url: /signup
|
||||
- link "Forgot your password?" [ref=e19] [cursor=pointer]:
|
||||
- /url: /forgot-password
|
||||
```
|
||||
|
After Width: | Height: | Size: 28 KiB |
@ -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]
|
||||
```
|
||||
|
After Width: | Height: | Size: 36 KiB |
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
166
tests/e2e/45a-career-profile.spec.mjs
Normal 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 });
|
||||
});
|
||||
});
|
||||
82
tests/e2e/45b-financial-profile.spec.mjs
Normal 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 });
|
||||
});
|
||||
});
|
||||
262
tests/e2e/45c-college-profile.spec.mjs
Normal 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(() => {});
|
||||
});
|
||||
});
|
||||
@ -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 we’re not showing the error text
|
||||
await expect(page.getByText(/Please pick a school from the list/i))
|
||||
.toHaveCount(0, { timeout: 2000 })
|
||||
.catch(() => {});
|
||||
});
|
||||
});
|
||||
@ -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; don’t 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 chart’s 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 });
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
157
tests/e2e/49-career-coach-quick-actions.spec.mjs
Normal 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 });
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
103
tests/e2e/50-resume-rewrite.spec.mjs
Normal 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 });
|
||||
});
|
||||
});
|
||||
155
tests/e2e/51-scenario-edit-modal.spec.mjs
Normal 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
|
||||
]);
|
||||
});
|
||||
});
|
||||