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({
|
export default defineConfig({
|
||||||
// Limit Playwright to E2E specs only
|
// Limit Playwright to E2E specs only
|
||||||
testDir: '/home/jcoakley/aptiva-dev1-app/tests/e2e',
|
testDir: '/home/jcoakley/aptiva-dev1-app/tests/e2e',
|
||||||
testMatch: /.*\.spec\.(?:mjs|js|ts)$/,
|
testMatch: /.*\.spec\.(?:mjs|js|ts)$/,
|
||||||
|
|
||||||
use: {
|
use: {
|
||||||
baseURL: process.env.PW_BASE_URL || 'http://localhost:3000',
|
baseURL: process.env.PW_BASE_URL || 'http://localhost:3000',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
},
|
},
|
||||||
retries: 1,
|
|
||||||
reporter: [['list'], ['html', { open: 'never' }]],
|
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 { useNavigate, useParams } from 'react-router-dom';
|
||||||
import apiFetch from '../auth/apiFetch.js';
|
import apiFetch from '../auth/apiFetch.js';
|
||||||
import moment from 'moment/moment.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
|
const authFetch = apiFetch; // keep local name, new implementation
|
||||||
/** -----------------------------------------------------------
|
/** -----------------------------------------------------------
|
||||||
@ -62,9 +64,11 @@ export default function CollegeProfileForm() {
|
|||||||
const [graduationTouched, setGraduationTouched] = useState(false);
|
const [graduationTouched, setGraduationTouched] = useState(false);
|
||||||
const [programLengthTouched, setProgramLengthTouched] = useState(false);
|
const [programLengthTouched, setProgramLengthTouched] = useState(false);
|
||||||
const [selectedUnitId, setSelectedUnitId] = useState(null);
|
const [selectedUnitId, setSelectedUnitId] = useState(null);
|
||||||
|
const [showAidWizard, setShowAidWizard] = useState(false);
|
||||||
const schoolPrevRef = useRef('');
|
const schoolPrevRef = useRef('');
|
||||||
const programPrevRef = useRef('');
|
const programPrevRef = useRef('');
|
||||||
const lastSchoolText = useRef('');
|
const lastSchoolText = useRef('');
|
||||||
|
const canonicalSchoolName = useRef(''); // the exact, server-known school name for selectedUnitId
|
||||||
|
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
@ -130,6 +134,9 @@ const onSchoolInput = async (e) => {
|
|||||||
selected_program: value ? prev.selected_program : '', // clear if user erased school
|
selected_program: value ? prev.selected_program : '', // clear if user erased school
|
||||||
program_type: ''
|
program_type: ''
|
||||||
}));
|
}));
|
||||||
|
setSelectedUnitId(null);
|
||||||
|
canonicalSchoolName.current = '';
|
||||||
|
setSchoolValid(value.trim() === ''); // empty = neutral, any text = invalid until validated
|
||||||
lastSchoolText.current = value;
|
lastSchoolText.current = value;
|
||||||
}
|
}
|
||||||
if (!value.trim()) { setSchoolSug([]); schoolPrevRef.current = ''; return; }
|
if (!value.trim()) { setSchoolSug([]); schoolPrevRef.current = ''; return; }
|
||||||
@ -199,8 +206,10 @@ const onProgramInput = async (e) => {
|
|||||||
})();
|
})();
|
||||||
}, [form.selected_school, form.selected_program]);
|
}, [form.selected_school, form.selected_program]);
|
||||||
|
|
||||||
// When selected_school changes (after commit/blur), reset program suggestions/types
|
|
||||||
useEffect(() => {
|
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([]);
|
setProgSug([]);
|
||||||
setTypes([]);
|
setTypes([]);
|
||||||
setSelectedUnitId(null);
|
setSelectedUnitId(null);
|
||||||
@ -229,6 +238,7 @@ const onProgramInput = async (e) => {
|
|||||||
setAutoTuition(Number.isFinite(n) ? n : 0);
|
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 profile came with school+program, load types so Degree Type select is populated
|
||||||
if ((normalized.selected_school || '') && (normalized.selected_program || '')) {
|
if ((normalized.selected_school || '') && (normalized.selected_program || '')) {
|
||||||
try {
|
try {
|
||||||
@ -245,51 +255,105 @@ const onProgramInput = async (e) => {
|
|||||||
}, [careerId, id]);
|
}, [careerId, id]);
|
||||||
|
|
||||||
|
|
||||||
async function handleSave(){
|
async function handleSave() {
|
||||||
try{
|
try {
|
||||||
|
|
||||||
// Compute chosen tuition exactly like Onboarding (manual override wins; blank => auto)
|
// Compute chosen tuition exactly like Onboarding (manual override wins; blank => auto)
|
||||||
const chosenTuition =
|
const chosenTuition =
|
||||||
(manualTuition.trim() === '')
|
(manualTuition.trim() === '')
|
||||||
? autoTuition
|
? autoTuition
|
||||||
: (Number.isFinite(parseFloat(manualTuition)) ? parseFloat(manualTuition) : autoTuition);
|
: (Number.isFinite(parseFloat(manualTuition)) ? parseFloat(manualTuition) : autoTuition);
|
||||||
|
|
||||||
// Confirm user actually picked from list (one alert, on Save only)
|
const schoolText = (form.selected_school || '').trim();
|
||||||
const school = (form.selected_school || '').trim().toLowerCase();
|
const progText = (form.selected_program || '').trim();
|
||||||
const prog = (form.selected_program || '').trim().toLowerCase();
|
const school = schoolText.toLowerCase();
|
||||||
// validate against current server suggestions (not local files)
|
const prog = progText.toLowerCase();
|
||||||
const exactSchool = school && schoolSug.find(o =>
|
|
||||||
(o.name || '').toLowerCase() === school
|
|
||||||
);
|
|
||||||
|
|
||||||
if (school && !exactSchool) {
|
// ---- 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);
|
setSchoolValid(false);
|
||||||
alert('Please pick a school from the list.');
|
alert('Please pick a school from the list.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const exactProgram = prog && progSug.find(p =>
|
|
||||||
(p.program || '').toLowerCase() === prog
|
|
||||||
);
|
// ---- PROGRAM validation ----
|
||||||
if (prog && !exactProgram) {
|
const progIsEmpty = progText === '';
|
||||||
|
if (progIsEmpty) {
|
||||||
setProgramValid(false);
|
setProgramValid(false);
|
||||||
alert('Please pick a program from the list.');
|
alert('Please pick a program from the list.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const body = normalisePayload({ ...form, tuition: chosenTuition, career_profile_id: careerId, unit_id: selectedUnitId ?? null });
|
|
||||||
const res = await authFetch('/api/premium/college-profile',{
|
let exactProgram = prog && progSug.find(p => (p.program || '').toLowerCase() === prog);
|
||||||
method:'POST',
|
|
||||||
headers:{'Content-Type':'application/json'},
|
// One-shot server confirm if suggestions are empty/stale
|
||||||
body:JSON.stringify(body)
|
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
|
||||||
});
|
});
|
||||||
if(!res.ok) throw new Error(await res.text());
|
|
||||||
|
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!');
|
alert('Saved!');
|
||||||
setForm(p => ({ ...p, tuition: chosenTuition }));
|
setForm(p => ({ ...p, tuition: chosenTuition }));
|
||||||
setManualTuition(String(chosenTuition));
|
setManualTuition(String(chosenTuition));
|
||||||
nav(-1);
|
nav(-1);
|
||||||
}catch(err){ console.error(err); alert(err.message);}
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert(err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sch = (form.selected_school || '').trim();
|
const sch = (form.selected_school || '').trim();
|
||||||
const prog = (form.selected_program || '').trim();
|
const prog = (form.selected_program || '').trim();
|
||||||
@ -329,7 +393,9 @@ const onProgramInput = async (e) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const chpy = Number(form.credit_hours_per_year);
|
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 ||
|
!form.program_type ||
|
||||||
!Number.isFinite(chpy) ||
|
!Number.isFinite(chpy) ||
|
||||||
chpy <= 0) {
|
chpy <= 0) {
|
||||||
@ -355,6 +421,7 @@ useEffect(() => {
|
|||||||
})();
|
})();
|
||||||
}, [
|
}, [
|
||||||
selectedUnitId,
|
selectedUnitId,
|
||||||
|
form.selected_school,
|
||||||
form.program_type,
|
form.program_type,
|
||||||
form.credit_hours_per_year,
|
form.credit_hours_per_year,
|
||||||
form.is_in_state,
|
form.is_in_state,
|
||||||
@ -482,9 +549,13 @@ return (
|
|||||||
setForm(prev => ({ ...prev, selected_school: exact.name }));
|
setForm(prev => ({ ...prev, selected_school: exact.name }));
|
||||||
}
|
}
|
||||||
if (!selectedUnitId) setSelectedUnitId(exact.unitId ?? null);
|
if (!selectedUnitId) setSelectedUnitId(exact.unitId ?? null);
|
||||||
|
canonicalSchoolName.current = exact.name;
|
||||||
|
setSchoolValid(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// Valid if empty (still choosing) OR exact chosen
|
// 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"
|
list="school-suggestions"
|
||||||
className={`w-full border rounded p-2 ${
|
className={`w-full border rounded p-2 ${
|
||||||
@ -639,14 +710,26 @@ return (
|
|||||||
className="w-full border rounded p-2"
|
className="w-full border rounded p-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium">Annual Aid</span>
|
<div className="space-y-1">
|
||||||
|
<label className="block font-medium">(Estimated) Annual Financial Aid</label>
|
||||||
|
<div className="flex space-x-2">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
name="annual_financial_aid"
|
name="annual_financial_aid"
|
||||||
value={form.annual_financial_aid}
|
value={form.annual_financial_aid}
|
||||||
onChange={handleFieldChange}
|
onChange={handleFieldChange}
|
||||||
className="mt-1 w-full border rounded p-2"
|
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 */}
|
{/* 8 │ Existing debt */}
|
||||||
@ -748,13 +831,24 @@ return (
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={!schoolValid || !programValid}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded"
|
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showAidWizard && (
|
||||||
|
<Modal onClose={() => setShowAidWizard(false)}>
|
||||||
|
<FinancialAidWizard
|
||||||
|
onAidEstimated={(estimate) => {
|
||||||
|
setForm(prev => ({ ...prev, annual_financial_aid: estimate }));
|
||||||
|
}}
|
||||||
|
onClose={() => setShowAidWizard(false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,21 @@
|
|||||||
{
|
{
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
"failedTests": [
|
"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.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 }) => {
|
test('create a new user via UI and persist creds for later specs', async ({ page }) => {
|
||||||
const u = uniq();
|
const u = uniq();
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 SignIn → Landing', () => {
|
test.describe('@p0 SignIn → Landing', () => {
|
||||||
test.setTimeout(20000);
|
test.setTimeout(10000);
|
||||||
|
|
||||||
test('signs in with persisted user and reaches SignInLanding', async ({ page }) => {
|
test('signs in with persisted user and reaches SignInLanding', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p1 Career Explorer — CareerSearch datalist', () => {
|
test.describe('@p1 Career Explorer — CareerSearch datalist', () => {
|
||||||
test.setTimeout(30000);
|
test.setTimeout(20000);
|
||||||
|
|
||||||
test('datalist commit opens modal; Change resets input', async ({ page }) => {
|
test('datalist commit opens modal; Change resets input', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Comparison: add → duplicate blocked → remove → persists', () => {
|
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 }) => {
|
test('add one, block duplicate, remove and persist', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Priorities modal — save & persist (current UI)', () => {
|
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 }) => {
|
test('open → choose answers → Save Answers → reload → same answers present', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Forgot Password — exact UI flow', () => {
|
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 }) => {
|
test('SignIn → Forgot → invalid email blocked → valid submit shows success copy', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
test.describe('@p0 Reset Password — exact UI flow', () => {
|
test.describe('@p0 Reset Password — exact UI flow', () => {
|
||||||
test.setTimeout(20000);
|
test.setTimeout(15000);
|
||||||
|
|
||||||
test('mismatch passwords → inline error, no request', async ({ page }) => {
|
test('mismatch passwords → inline error, no request', async ({ page }) => {
|
||||||
// Route with a token param (the component checks presence only)
|
// 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';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 PremiumRoute guard', () => {
|
test.describe('@p0 PremiumRoute guard', () => {
|
||||||
test.setTimeout(20000);
|
test.setTimeout(15000);
|
||||||
|
|
||||||
test('non-premium user is redirected to /paywall from premium routes', async ({ page }) => {
|
test('non-premium user is redirected to /paywall from premium routes', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
test.describe('@p0 SessionExpiredHandler', () => {
|
test.describe('@p0 SessionExpiredHandler', () => {
|
||||||
test.setTimeout(20000);
|
test.setTimeout(15000);
|
||||||
|
|
||||||
test('unauth → protected route → /signin?session=expired + banner', async ({ page }) => {
|
test('unauth → protected route → /signin?session=expired + banner', async ({ page }) => {
|
||||||
await page.context().clearCookies();
|
await page.context().clearCookies();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Support modal — open/close', () => {
|
test.describe('@p0 Support modal — open/close', () => {
|
||||||
test.setTimeout(20000);
|
test.setTimeout(15000);
|
||||||
|
|
||||||
test('header Support opens modal and can be closed', async ({ page }) => {
|
test('header Support opens modal and can be closed', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Paywall CTA', () => {
|
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 }) => {
|
test('Upgrade to Premium visible on non-premium pages and navigates to /paywall', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Support — submit ticket', () => {
|
test.describe('@p0 Support — submit ticket', () => {
|
||||||
test.setTimeout(20000);
|
test.setTimeout(15000);
|
||||||
|
|
||||||
test('open → fill form → submit → success (network or UI)', async ({ page }) => {
|
test('open → fill form → submit → success (network or UI)', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Header nav menus', () => {
|
test.describe('@p0 Header nav menus', () => {
|
||||||
test.setTimeout(20000);
|
test.setTimeout(15000);
|
||||||
|
|
||||||
test('Find Your Career menu → Career Explorer & Interest Inventory', async ({ page }) => {
|
test('Find Your Career menu → Career Explorer & Interest Inventory', async ({ page }) => {
|
||||||
const u = loadTestUser();
|
const u = loadTestUser();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Profile menu gating (non-premium)', () => {
|
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 }) => {
|
test('Career/College Profiles show disabled labels when user is not premium', async ({ page }) => {
|
||||||
const u = loadTestUser();
|
const u = loadTestUser();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Support — burst rate limit', () => {
|
test.describe('@p0 Support — burst rate limit', () => {
|
||||||
test.setTimeout(20000);
|
test.setTimeout(15000);
|
||||||
|
|
||||||
test('rapid submissions eventually return 429 Too Many Requests', async ({ page }) => {
|
test('rapid submissions eventually return 429 Too Many Requests', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
const user = loadTestUser();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Support — auth + dedupe', () => {
|
test.describe('@p0 Support — auth + dedupe', () => {
|
||||||
test.setTimeout(20000);
|
test.setTimeout(15000);
|
||||||
|
|
||||||
test('unauthenticated request is rejected (401)', async ({ page }) => {
|
test('unauthenticated request is rejected (401)', async ({ page }) => {
|
||||||
await page.context().clearCookies();
|
await page.context().clearCookies();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p0 Educational Programs — handoff & page render', () => {
|
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 }) => {
|
test('handoff carries selectedCareer; page shows career title, KSA header and school cards; survives reload', async ({ page }) => {
|
||||||
const user = loadTestUser();
|
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 { test, expect } from '@playwright/test';
|
||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p1 CareerRoadmap — panels + Coach basics', () => {
|
const j = (o) => JSON.stringify(o);
|
||||||
test.setTimeout(60000);
|
|
||||||
|
|
||||||
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();
|
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.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('Username', { exact: true }).fill(u.username);
|
||||||
await page.getByPlaceholder('Password', { exact: true }).fill(u.password);
|
await page.getByPlaceholder('Password', { exact: true }).fill(u.password);
|
||||||
await page.getByRole('button', { name: /^Sign In$/ }).click();
|
await page.getByRole('button', { name: /^Sign In$/ }).click();
|
||||||
await page.waitForURL('**/signin-landing**', { timeout: 15000 });
|
await page.waitForURL('**/signin-landing**', { timeout: 15000 });
|
||||||
|
|
||||||
const scen = await page.request.post('/api/premium/career-profile', {
|
// ── Seed a scenario; capture id
|
||||||
data: { career_name: 'QA Roadmap', status: 'planned', start_date: '2025-09-01' }
|
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();
|
const { career_profile_id } = await scen.json();
|
||||||
|
|
||||||
await page.goto(`/career-roadmap/${career_profile_id}`, { waitUntil: 'networkidle' });
|
// ── Deterministic reads CareerRoadmap expects
|
||||||
await expect(page.getByText(/Where you are now and where you are going/i)).toBeVisible({ timeout: 20000 });
|
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();
|
// Scenario row: include soc_code to skip /api/careers/resolve
|
||||||
await page.getByPlaceholder('Ask your Career Coach…').fill('Give me one tip.');
|
await page.route(new RegExp(`/api/premium/career-profile/${career_profile_id}$`, 'i'), r => r.fulfill({
|
||||||
await page.getByRole('button', { name: /^Send$/ }).click();
|
status: 200, contentType: 'application/json',
|
||||||
await expect(page.getByText(/Coach is typing…/i)).toBeVisible();
|
body: j({
|
||||||
await expect(page.getByText(/Coach is typing…/i)).toHaveCount(0, { timeout: 25000 });
|
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 { test, expect } from '@playwright/test';
|
||||||
import { loadTestUser } from '../utils/testUser.js';
|
import { loadTestUser } from '../utils/testUser.js';
|
||||||
|
|
||||||
test.describe('@p1 Milestones — create (modal), edit, delete', () => {
|
const j = (o) => JSON.stringify(o);
|
||||||
test.setTimeout(60000);
|
|
||||||
|
|
||||||
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();
|
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.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('Username', { exact: true }).fill(u.username);
|
||||||
await page.getByPlaceholder('Password', { exact: true }).fill(u.password);
|
await page.getByPlaceholder('Password', { exact: true }).fill(u.password);
|
||||||
await page.getByRole('button', { name: /^Sign In$/ }).click();
|
await page.getByRole('button', { name: /^Sign In$/ }).click();
|
||||||
await page.waitForURL('**/signin-landing**', { timeout: 15000 });
|
await page.waitForURL('**/signin-landing**', { timeout: 15000 });
|
||||||
|
|
||||||
const scen = await page.request.post('/api/premium/career-profile', {
|
// ── Seed scenario
|
||||||
data: { career_name: 'QA Milestones', status: 'planned', start_date: '2025-09-01' }
|
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();
|
const { career_profile_id } = await scen.json();
|
||||||
|
|
||||||
await page.goto(`/career-roadmap/${career_profile_id}`, { waitUntil: 'networkidle' });
|
// ── Minimal background routes for CareerRoadmap
|
||||||
// Milestones section anchor
|
await page.route(new RegExp(`/api/premium/career-profile/${career_profile_id}$`, 'i'), r =>
|
||||||
await expect(page.getByRole('heading', { name: /^Milestones$/ })).toBeVisible({ timeout: 20000 });
|
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
|
await page.route(/\/api\/premium\/financial-profile$/i, r => r.fulfill({
|
||||||
const addDetails = page.getByRole('button', { name: /Add Details/i });
|
status: 200, contentType: 'application/json',
|
||||||
if (await addDetails.isVisible().catch(() => false)) {
|
body: j({
|
||||||
await addDetails.click();
|
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 {
|
} else {
|
||||||
// Fallback: open the modal through any visible “Milestones” action button
|
await createDate.fill('');
|
||||||
const anyBtn = page.getByRole('button', { name: /Add|Edit|Milestone/i }).first();
|
await createDate.type('05/01/2026', { delay: 10 });
|
||||||
await anyBtn.click().catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal header
|
await newSection.locator('label:has-text("Description")').locator('..').locator('textarea').first()
|
||||||
await expect(page.getByRole('heading', { name: /^Milestones$/ })).toBeVisible({ timeout: 15000 });
|
.fill('Ship v1 and announce.');
|
||||||
|
|
||||||
// Add new milestone section
|
// Impact (salary)
|
||||||
await page.getByText(/Add new milestone/i).click();
|
await newSection.getByRole('button', { name: /^\+ Add impact$/ }).click();
|
||||||
await page.getByLabel(/^Title$/i).first().fill('QA Milestone A');
|
await newSection.locator('label:has-text("Type")').locator('..').locator('select').last().selectOption('salary');
|
||||||
await page.getByLabel(/^Date$/i).first().fill('2026-04-01');
|
await newSection.locator('label:has-text("Amount")').locator('..').locator('input[type="number"]').last().fill('12000');
|
||||||
await page.getByRole('button', { name: /\+ Add impact/i }).click();
|
await newSection.locator('label:has-text("Start")').locator('..').locator('input[type="date"]').last().fill('2026-05-01');
|
||||||
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');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: /Save milestone/i }).click();
|
// One task
|
||||||
await expect(page.getByText(/QA Milestone A/i)).toBeVisible({ timeout: 20000 });
|
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
|
// Save
|
||||||
await page.getByRole('button', { name: /QA Milestone A/i }).click();
|
await modalRoot.getByRole('button', { name: /^Save milestone$/i }).click();
|
||||||
await page.getByLabel(/Description/i).fill('Short description');
|
|
||||||
await page.getByRole('button', { name: /^Save$/ }).click();
|
|
||||||
|
|
||||||
// Delete milestone
|
// Ensure listed in panel and visible — expand the exact <details> that owns the row
|
||||||
await page.getByRole('button', { name: /Delete milestone/i }).click();
|
const createdRowText = page.getByText('Launch Portfolio Website').first();
|
||||||
// Close modal
|
const createdDetails = createdRowText.locator('xpath=ancestor::details[1]');
|
||||||
await page.getByRole('button', { name: /^Close$/ }).click();
|
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
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||