Prod test run fixes v1.

This commit is contained in:
Josh 2025-09-18 13:26:16 +00:00
parent 219493e1b0
commit 20a1f796b5
90 changed files with 531 additions and 1113 deletions

View File

@ -1 +1 @@
767a2e51259e707655c80d6449afa93abf982fec-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
408b293acaaa053b934050f88b9c93db41ecb097-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b

View File

@ -8,7 +8,7 @@ const __dirname = path.dirname(__filename);
// repo root = two levels up from /backend/config
const repoRoot = path.resolve(__dirname, '..', '..');
const env = (process.env.NODE_ENV || 'development').trim();
const env = (process.env.ENV_NAME || 'prod').trim();
// Prefer .env.development / .env.production — fall back to plain .env
const fileA = path.join(repoRoot, `.env.${env}`);

View File

@ -27,7 +27,7 @@ const CANARY_SQL = `
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootPath = path.resolve(__dirname, '..');
const env = process.env.NODE_ENV?.trim() || 'development';
const isProd = (process.env.ENV_NAME === 'prod');
const envPath = path.resolve(rootPath, `.env.${env}`);
dotenv.config({ path: envPath, override: false });
@ -778,7 +778,10 @@ app.post('/api/auth/verify/email/send', requireAuth, verifySendLimiter, async (r
text,
html: `<pre style="font-family: ui-monospace, Menlo, monospace; white-space: pre-wrap">${text}</pre>`
});
return res.status(200).json({ ok: true });
// In non-production, include token to enable E2E to complete verification without email I/O.
const extra = (process.env.ENV_NAME === 'prod') ? {} : { test_token: token };
return res.status(200).json({ ok: true, ...extra });
} catch (e) {
console.error('[verify/email/send]', e?.message || e);
return res.status(500).json({ error: 'Failed to send verification email' });
@ -1092,7 +1095,7 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
career_list = ?,
phone_e164 = ?,
sms_opt_in = ?,
sms_reminders_opt_in = ?
sms_reminders_opt_in = ?,
sms_reminders_opt_in_at =
CASE
WHEN ? = 1 AND (sms_reminders_opt_in IS NULL OR sms_reminders_opt_in = 0)
@ -1118,6 +1121,7 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
phoneFinal,
smsOptFinal,
smsRemindersFinal,
smsRemindersFinal,
profileId
];
@ -1125,14 +1129,16 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
return res.status(200).json({ message: 'User profile updated successfully' });
} else {
// INSERT branch
const insertQuery = `
const insertQuery = `
INSERT INTO user_profile
(id, username, firstname, lastname, email, email_lookup, zipcode, state, area,
career_situation, interest_inventory_answers, riasec_scores,
career_priorities, career_list, phone_e164, sms_opt_in, sms_reminders_opt_in)
career_situation, interest_inventory_answers, riasec_scores,
career_priorities, career_list, phone_e164, sms_opt_in, sms_reminders_opt_in,
sms_reminders_opt_in_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?,
?, ?, ?, ?, ?)
?, ?, ?,
?, ?, ?, ?, ?,
CASE WHEN ? = 1 THEN UTC_TIMESTAMP() ELSE NULL END)
`;
const params = [
profileId,
@ -1151,7 +1157,8 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
finalCareerList,
phoneFinal,
smsOptFinal,
smsRemindersFinal
smsRemindersFinal,
smsRemindersFinal,
];

View File

@ -32,7 +32,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootPath = path.resolve(__dirname, '..');
const env = process.env.NODE_ENV?.trim() || 'development';
const isProd = (process.env.ENV_NAME === 'prod');
const envPath = path.resolve(rootPath, `.env.${env}`);
dotenv.config({ path: envPath, override: false }); // don't clobber compose-injected env
@ -46,7 +46,7 @@ const DB_POOL_SIZE = 6;
const API_BASE = (process.env.APTIVA_INTERNAL_API || 'http://server1:5000').replace(/\/+$/, '');
const REQUIRED_FILES = [CIP_TO_SOC_PATH, INSTITUTION_DATA_PATH, SALARY_DB_PATH];
if (process.env.NODE_ENV !== 'production') REQUIRED_FILES.push(USER_PROFILE_DB_PATH);
if (process.env.ENV_NAME !== 'prod') REQUIRED_FILES.push(USER_PROFILE_DB_PATH);
for (const p of REQUIRED_FILES) {
if (!fs.existsSync(p)) {
console.error(`FATAL Required data file not found → ${p}`);

View File

@ -34,7 +34,7 @@ import './jobs/reminderCron.js';
import { cacheSummary } from "./utils/ctxCache.js";
const rootPath = path.resolve(__dirname, '..');
const env = (process.env.NODE_ENV || 'prod');
const env = (process.env.ENV_NAME || 'prod');
const envPath = path.resolve(rootPath, `.env.${env}`);
if (!process.env.FROM_SECRETS_MANAGER) {
dotenv.config({ path: envPath, override: false });
@ -602,7 +602,7 @@ app.post(
return res.status(400).end();
}
// Env guard: only handle events matching our env
const isProd = (process.env.NODE_ENV === 'prod');
const isProd = (process.env.ENV_NAME === 'prod');
if (Boolean(event.livemode) !== isProd) {
console.warn('[Stripe] Ignoring webhook due to livemode mismatch', { livemode: event.livemode, isProd });
return res.sendStatus(200);
@ -1455,14 +1455,17 @@ I'm here to support you with personalized coaching—what would you like to focu
salaryAnalysis = null,
economicProjections = null
}) {
const _userProfile = userProfile || {};
const _scenarioRow = scenarioRow || {};
const _financialProfile = financialProfile || {};
const _collegeProfile = collegeProfile || {};
// 1) USER PROFILE
const firstName = userProfile.firstname || "N/A";
const lastName = userProfile.lastname || "N/A";
const fullName = `${firstName} ${lastName}`;
const username = userProfile.username || "N/A";
const location = userProfile.area || userProfile.state || "Unknown Region";
// userProfile.career_situation might be "enhancing", "preparing", etc.
const careerSituation = userProfile.career_situation || "Not provided";
const username = _userProfile.username || "N/A";
const location = _userProfile.area || _userProfile.state || "Unknown Region";
const careerSituation = _userProfile.career_situation || "Not provided";
// RIASEC
let riasecText = "None";
@ -1485,10 +1488,10 @@ I'm here to support you with personalized coaching—what would you like to focu
// Possibly parse "career_priorities" if you need them
let careerPriorities = "Not provided";
if (userProfile.career_priorities) {
if (_userProfile.career_priorities) {
// e.g. "career_priorities": "{\"interests\":\"Somewhat important\",\"meaning\":\"Somewhat important\",\"stability\":\"Very important\", ...}"
try {
const cP = JSON.parse(userProfile.career_priorities);
const cP = JSON.parse(_userProfile.career_priorities);
// Build a bullet string
careerPriorities = Object.entries(cP).map(([k,v]) => `- ${k}: ${v}`).join("\n");
} catch(e) {
@ -1499,30 +1502,29 @@ I'm here to support you with personalized coaching—what would you like to focu
// 2) CAREER SCENARIO
// scenarioRow might have career_name, job_description, tasks
// but you said sometimes you store them in scenarioRow or pass them in a separate param
const careerName = scenarioRow.career_name || "No career selected";
const socCode = scenarioRow.soc_code || "N/A";
const jobDescription = scenarioRow.job_description || "No jobDescription info";
// scenarioRow.tasks might be an array
const tasksList = Array.isArray(scenarioRow.tasks) && scenarioRow.tasks.length
? scenarioRow.tasks.join(", ")
const careerName = _scenarioRow.career_name || "No career selected";
const socCode = _scenarioRow.soc_code || "N/A";
const jobDescription = _scenarioRow.job_description || "No jobDescription info";
const tasksList = Array.isArray(_scenarioRow.tasks) && _scenarioRow.tasks.length
? _scenarioRow.tasks.join(", ")
: "No tasks info";
// 3) FINANCIAL PROFILE
// your actual JSON uses e.g. "current_salary", "additional_income"
const currentSalary = financialProfile.current_salary || 0;
const additionalIncome = financialProfile.additional_income || 0;
const monthlyExpenses = financialProfile.monthly_expenses || 0;
const monthlyDebt = financialProfile.monthly_debt_payments || 0;
const retirementSavings = financialProfile.retirement_savings || 0;
const emergencyFund = financialProfile.emergency_fund || 0;
const currentSalary = _financialProfile.current_salary || 0;
const additionalIncome = _financialProfile.additional_income || 0;
const monthlyExpenses = _financialProfile.monthly_expenses || 0;
const monthlyDebt = _financialProfile.monthly_debt_payments || 0;
const retirementSavings = _financialProfile.retirement_savings || 0;
const emergencyFund = _financialProfile.emergency_fund || 0;
// 4) COLLEGE PROFILE
// from your JSON:
const selectedProgram = collegeProfile.selected_program || "N/A";
const enrollmentStatus = collegeProfile.college_enrollment_status || "Not enrolled";
const creditHoursCompleted = parseFloat(collegeProfile.hours_completed) || 0;
const programLength = parseFloat(collegeProfile.program_length) || 0;
const expectedGraduation = collegeProfile.expected_graduation || "Unknown";
const selectedProgram = _collegeProfile?.selected_program ?? "N/A";
const enrollmentStatus = _collegeProfile?.college_enrollment_status ?? "Not enrolled";
const creditHoursCompleted = parseFloat(_collegeProfile?.hours_completed ?? 0) || 0;
const programLength = parseFloat(_collegeProfile?.program_length ?? 0) || 0;
const expectedGraduation = _collegeProfile?.expected_graduation ?? "Unknown";
// 5) AI RISK
// from aiRisk object
@ -1678,7 +1680,7 @@ let summaryText = buildUserSummary({
scenarioRow,
userProfile,
financialProfile,
collegeProfile,
collegeProfile: collegeProfile || {},
aiRisk
});
@ -2502,6 +2504,26 @@ app.post('/api/premium/career-profile/clone', authenticatePremiumUser, async (re
[newId, ...values]
);
// 2.5) copy ALL college_profiles tied to the source scenario
const [cprows] = await pool.query(
'SELECT * FROM college_profiles WHERE career_profile_id=? AND user_id=?',
[sourceId, req.id]
);
for (const cp of cprows) {
const newCpId = uuidv4();
const cols = Object.keys(cp).filter(k => !['id','created_at','updated_at'].includes(k));
const vals = cols.map(k =>
k === 'career_profile_id' ? newId :
k === 'user_id' ? req.id :
cp[k]
);
await pool.query(
`INSERT INTO college_profiles (id, ${cols.join(',')})
VALUES (?, ${cols.map(() => '?').join(',')})`,
[newCpId, ...vals]
);
}
// 3) copy milestones/tasks/impacts (optional mirrors UI wizard)
const [mils] = await pool.query(
'SELECT * FROM milestones WHERE career_profile_id=? AND user_id=?',
@ -3631,11 +3653,11 @@ app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res
const { careerProfileId } = req.query;
try {
const [rows] = await pool.query(
`SELECT *
`SELECT *
FROM college_profiles
WHERE user_id = ?
AND career_profile_id = ?
ORDER BY created_at DESC
ORDER BY updated_at DESC
LIMIT 1`,
[req.id, careerProfileId]
);

View File

@ -10,7 +10,7 @@ import path from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootPath = path.resolve(__dirname, "../..");
const env = process.env.NODE_ENV?.trim() || "development";
const env = process.env.ENV_NAME?.trim() || "prod";
dotenv.config({ path: path.resolve(rootPath, `.env.${env}`) });
const faqPath = path.resolve(rootPath, "backend", "data", "faqs.json");

Binary file not shown.

View File

@ -88,6 +88,11 @@ http {
# ───── React static assets ─────
root /usr/share/nginx/html;
index index.html;
# Redirect only the bare root to /signin (avoid booting shell at '/')
location = / {
return 302 /signin$is_args$args;
}
location / {
try_files $uri $uri/ /index.html;
}

View File

@ -0,0 +1,58 @@
# 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 "Verify your account" [level=1] [ref=e25]
- paragraph [ref=e26]: You must verify before using AptivaAI.
- generic [ref=e27]:
- heading "Email verification" [level=2] [ref=e28]
- generic [ref=e29]:
- button "Send email" [ref=e30] [cursor=pointer]
- textbox "Paste token" [ref=e31]
- button "Confirm" [ref=e32] [cursor=pointer]
- generic [ref=e33]:
- heading "Phone verification (optional)" [level=2] [ref=e34]
- generic [ref=e35]:
- checkbox "By requesting a code, you agree to receive one-time texts from AptivaAI for account verification and security alerts. Message frequency varies. Msg & data rates may apply. Reply STOP to opt out, HELP for help. Consent is not a condition of purchase or service. See the SMS Terms, Privacy Policy, and Terms." [ref=e36]
- generic [ref=e37]:
- text: By requesting a code, you agree to receive one-time texts from AptivaAI for account verification and security alerts. Message frequency varies. Msg & data rates may apply. Reply
- strong [ref=e38]: STOP
- text: to opt out,
- strong [ref=e39]: HELP
- text: for help. Consent is not a condition of purchase or service. See the
- link "SMS Terms" [ref=e40] [cursor=pointer]:
- /url: /sms
- text: ","
- link "Privacy Policy" [ref=e41] [cursor=pointer]:
- /url: /legal/privacy
- text: ", and"
- link "Terms" [ref=e42] [cursor=pointer]:
- /url: /legal/terms
- text: .
- generic [ref=e43]:
- textbox "+1XXXXXXXXXX" [ref=e44]: "1"
- button "Send code" [disabled] [ref=e45]
- generic [ref=e46]:
- textbox "6-digit code" [ref=e47]
- button "Confirm" [disabled] [ref=e48]
- button "Open chat" [ref=e49] [cursor=pointer]:
- img [ref=e50] [cursor=pointer]
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

File diff suppressed because one or more lines are too long

View File

@ -23,5 +23,7 @@ export default defineConfig({
use: { ...devices['Desktop Edge'] },
},
],
// Perform a single real-UI login before the test run and write storage state.
globalSetup: '/home/jcoakley/aptiva-dev1-app/tests/e2e/global-setup.mjs',
});

View File

@ -337,7 +337,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
const { careerId } = useParams();
const location = useLocation();
const [interestStrategy, setInterestStrategy] = useState('FLAT'); // 'NONE' | 'FLAT' | 'MONTE_CARLO'
const [interestStrategy, setInterestStrategy] = useState('FLAT'); // 'NONE' | 'FLAT' | 'RANDOM'
const [flatAnnualRate, setFlatAnnualRate] = useState(0.06);
const [randomRangeMin, setRandomRangeMin] = useState(-0.02);
const [randomRangeMax, setRandomRangeMax] = useState(0.02);
@ -1665,7 +1665,7 @@ const handleMilestonesCreated = useCallback(
>
<option value="NONE">No Interest</option>
<option value="FLAT">Flat Rate</option>
<option value="MONTE_CARLO">Random</option>
<option value="RANDOM">Random</option>
</select>
{/* (E2) If FLAT => show the annual rate */}
@ -1682,8 +1682,8 @@ const handleMilestonesCreated = useCallback(
</div>
)}
{/* (E3) If MONTE_CARLO => show the random range */}
{interestStrategy === 'MONTE_CARLO' && (
{/* (E3) If RANDOM => show the random range */}
{interestStrategy === 'RANDOM' && (
<div className="inline-block ml-4">
<label className="mr-1">Min Return (%):</label>
<input

View File

@ -99,10 +99,18 @@ export default function RetirementPlanner () {
}
async function handleCloneScenario (src) {
/* bring over the original long clone implementation here or import
from a helper if you already abstracted it. Leaving a stub so
the UI compiles. */
alert('Clone scenario not wired yet');
if (!src?.id) return;
try {
const r = await authFetch('/api/premium/career-profile/clone', {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify({ sourceId: src.id, overrides: {} })
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
await loadAll(); // refresh scenarios list in state
} catch (e) {
alert(`Clone scenario failed (${e.message})`);
}
}
/* ------------------ chat patch helper -------------------------- */

View File

@ -114,10 +114,23 @@ export default function ScenarioContainer({
setLocalScenario(scenario || null);
}, [scenario]);
function handleScenarioSelect(e) {
async function handleScenarioSelect(e) {
const chosenId = e.target.value;
const found = allScenarios.find((s) => s.id === chosenId);
// optimistic set so UI updates immediately
setLocalScenario(found || null);
// hydrate with full scenario so KPIs/sim have all fields
if (chosenId) {
try {
const res = await authFetch(`/api/premium/career-profile/${chosenId}`);
if (res.ok) {
const full = await res.json();
setLocalScenario((prev) => ({ ...(prev || {}), ...(full || {}) }));
}
} catch (err) {
console.error('hydrate scenario on select failed:', err);
}
}
}
// -------------------------------------------------------------
@ -203,6 +216,16 @@ export default function ScenarioContainer({
fetchMilestones();
}, [fetchMilestones]);
// Helper: find a "Retirement" milestone date (YYYY-MM-DD) if present
function retirementDateFromMilestone() {
if (!Array.isArray(milestones) || !milestones.length) return null;
const ms = milestones
.filter(m => m?.date && typeof m.title === 'string' && m.title.trim().toLowerCase() === 'retirement')
.sort((a, b) => new Date(a.date) - new Date(b.date));
return ms[0]?.date || null;
}
// -------------------------------------------------------------
// 4) Simulation
// -------------------------------------------------------------
@ -231,18 +254,26 @@ export default function ScenarioContainer({
// Gather milestoneImpacts
const allImpacts = Object.values(impactsByMilestone).flat(); // safe even if []
const simYears = parseInt(simulationYearsInput, 10) || 20;
const simYears = parseInt(simulationYearsInput, 10) || 20;
const simYearsUI = Math.max(1, parseInt(simulationYearsInput, 10) || 20);
const yearsUntilRet = localScenario.retirement_start_date
? Math.ceil(
moment(localScenario.retirement_start_date)
.startOf('month')
.diff(moment().startOf('month'), 'months') / 12
)
: 0;
// Derive effective retirement start date: scenario field → milestone → projected_end_date+1mo
const mRet = retirementDateFromMilestone();
const effectiveRetStart =
localScenario.retirement_start_date
|| (mRet ? moment(mRet).startOf('month').format('YYYY-MM-DD') : null)
|| (localScenario.projected_end_date
? moment(localScenario.projected_end_date).startOf('month').add(1, 'month').format('YYYY-MM-DD')
: null);
const simYearsEngine = Math.max(simYearsUI, yearsUntilRet + 1);
const yearsUntilRet = effectiveRetStart
? Math.ceil(
moment(effectiveRetStart).startOf('month')
.diff(moment().startOf('month'), 'months') / 12
)
: 0;
const simYearsEngine = Math.max(simYearsUI, yearsUntilRet + 1);
// scenario overrides
const scenarioOverrides = {
@ -311,14 +342,7 @@ export default function ScenarioContainer({
surplusEmergencyAllocation: scenarioOverrides.surplusEmergencyAllocation,
surplusRetirementAllocation: scenarioOverrides.surplusRetirementAllocation,
additionalIncome: scenarioOverrides.additionalIncome,
retirement_start_date:
localScenario.retirement_start_date // user picked
|| (localScenario.projected_end_date // often set for college scenarios
? moment(localScenario.projected_end_date)
.startOf('month')
.add(1,'month') // start drawing a month later
.format('YYYY-MM-DD')
: null),
retirement_start_date: effectiveRetStart,
desired_retirement_income_monthly:
parseScenarioOverride(
@ -844,16 +868,53 @@ export default function ScenarioContainer({
// -------------------------------------------------------------
// 9) Scenario Edit
// -------------------------------------------------------------
function handleEditScenario() {
async function handleEditScenario() {
// Ensure modal gets a fully populated scenario (incl. desired_retirement_income_monthly)
if (localScenario?.id) {
try {
const res = await authFetch(`/api/premium/career-profile/${localScenario.id}`);
if (res.ok) {
const full = await res.json();
setLocalScenario((prev) => ({ ...(prev || {}), ...(full || {}) }));
}
} catch (e) {
console.error('load scenario for edit', e);
// non-fatal: open modal with existing fields
}
}
setShowScenarioModal(true);
}
function handleScenarioSave(updated) {
console.log('TODO => Save scenario', updated);
setShowScenarioModal(false);
async function handleScenarioSave(updated) {
if (!localScenario?.id) return;
try {
const res = await authFetch(`/api/premium/career-profile/${localScenario.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updated)
});
if (!res.ok) {
const txt = await res.text();
alert(txt || 'Failed to save scenario');
return;
}
const saved = await res.json();
// Keep local view in sync; prefer the user's edited values if API didn't echo them back
setLocalScenario((prev) => ({ ...(prev || {}), ...(saved || {}), ...(updated || {}) }));
// Refresh milestones if dates/flags shifted any annotations
await fetchMilestones();
} catch (e) {
console.error('Scenario save error:', e);
alert('Error saving scenario');
} finally {
setShowScenarioModal(false);
}
}
function handleDeleteScenario() {
if (localScenario) onRemove(localScenario.id);
}
function handleCloneScenario() {
if (localScenario) onClone(localScenario);
}
@ -863,7 +924,7 @@ export default function ScenarioContainer({
// -------------------------------------------------------------
return (
<article
onClick={() => onSelect(localScenario.id)}
onClick={() => { if (typeof onSelect === 'function' && localScenario?.id) onSelect(localScenario.id); }}
className="w-full md:max-w-md border p-3 pb-4 rounded bg-white
hover:shadow transition-shadow"
>
@ -883,6 +944,13 @@ return (
{localScenario && (
<>
{/* snapshot note */}
{collegeProfile && (
<p className="text-xs text-gray-500 mt-1">
Using your most recently <span className="lowercase">updated</span> college plan
{collegeProfile.updated_at ? ` (${moment(collegeProfile.updated_at).format('YYYY-MM')})` : ''}.
</p>
)}
{/* ───────────── Title ───────────── */}
<h4
className="font-semibold text-lg leading-tight truncate"
@ -911,7 +979,7 @@ return (
>
<option value="NONE">No Interest</option>
<option value="FLAT">Flat Rate</option>
<option value="MONTE_CARLO">Random</option>
<option value="RANDOM">Random</option>
</select>
{interestStrategy === 'FLAT' && (
@ -929,7 +997,7 @@ return (
</span>
)}
{interestStrategy === 'MONTE_CARLO' && (
{interestStrategy === 'RANDOM' && (
<span className="ml-2 space-x-1">
<label>Min %:</label>
<input
@ -959,8 +1027,7 @@ return (
<div className="relative h-56 sm:h-64 md:h-72 my-4 px-1">
<Line data={chartData} options={chartOptions} />
</div>
{(!localScenario?.retirement_start_date ||
{(!(localScenario?.retirement_start_date || retirementDateFromMilestone()) ||
!localScenario?.desired_retirement_income_monthly) && (
<div className="bg-yellow-100 border-l-4 border-yellow-500 p-3 rounded mb-3 text-sm">
<p className="text-gray-800">
@ -982,8 +1049,16 @@ return (
<div className="space-y-1 text-sm">
{/* Nest-egg */}
<p className="uppercase text-gray-500 text-[11px] tracking-wide">Nest Egg</p>
<p className="text-lg font-semibold">{usd(retireBalAtMilestone)}</p>
{(() => {
// Prefer TOTAL savings at the retirement start month; fall back to retirement-only
const retStart = localScenario?.retirement_start_date
? moment(localScenario.retirement_start_date).startOf('month').format('YYYY-MM')
: null;
const idx = retStart ? chartLabels.indexOf(retStart) : -1;
const totalAtRet = idx >= 0 ? projectionData[idx]?.totalSavings : null;
const nestEgg = (typeof totalAtRet === 'number' ? totalAtRet : retireBalAtMilestone) || 0;
return <p className="text-lg font-semibold">{usd(nestEgg)}</p>;
})()}
{/* Money lasts */}
<p className="uppercase text-gray-500 text-[11px] tracking-wide mt-2">Money Lasts</p>
<p className="text-lg font-semibold inline-flex items-center">

View File

@ -90,28 +90,6 @@ function SignUp() {
{ name: 'West Virginia', code: 'WV' }, { name: 'Wisconsin', code: 'WI' }, { name: 'Wyoming', code: 'WY' },
];
useEffect(() => {
const fetchAreas = async () => {
if (!state) {
setAreas([]);
return;
}
setLoadingAreas(true); // Start loading
try {
const res = await fetch(`/api/areas?state=${state}`);
const data = await res.json();
setAreas(data.areas || []);
} catch (err) {
console.error('Error fetching areas:', err);
setAreas([]);
} finally {
setLoadingAreas(false); // Done loading
}
};
fetchAreas();
}, [state]);
const validateFields = async () => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
@ -241,59 +219,35 @@ const handleSituationConfirm = async () => {
};
useEffect(() => {
// reset UI
setAreasErr('');
if (!state) { setAreas([]); return; }
setAreasErr('');
if (!state) { setAreas([]); setArea(''); return; }
// cached? instant
if (areasCacheRef.current.has(state)) {
setAreas(areasCacheRef.current.get(state));
return;
// cancel previous request if any
if (inflightRef.current) inflightRef.current.abort();
const controller = new AbortController();
inflightRef.current = controller;
setLoadingAreas(true);
(async () => {
try {
const res = await fetch(`/api/areas?state=${encodeURIComponent(state)}`, { signal: controller.signal });
if (!res.ok) throw new Error('bad_response');
const data = await res.json();
const list = Array.isArray(data?.areas) ? data.areas : [];
setAreas(list);
if (area && !list.includes(area)) setArea('');
} catch (err) {
if (controller.signal.aborted) return; // superseded by a newer request
setAreas([]);
setAreasErr('Could not load Areas. Please try again.');
} finally {
if (inflightRef.current === controller) inflightRef.current = null;
setLoadingAreas(false);
}
})();
// debounce to avoid rapid refetch on quick clicks
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(async () => {
// cancel previous request if any
if (inflightRef.current) inflightRef.current.abort();
const controller = new AbortController();
inflightRef.current = controller;
setLoadingAreas(true);
try {
// client-side timeout race (6s)
const timeout = new Promise((_, rej) =>
setTimeout(() => rej(new Error('timeout')), 6000)
);
const res = await Promise.race([
fetch(`/api/areas?state=${encodeURIComponent(state)}`, {
signal: controller.signal,
}),
timeout,
]);
if (!res || !res.ok) throw new Error('bad_response');
const data = await res.json();
// normalize, uniq, sort for UX
const list = Array.from(new Set((data.areas || []).filter(Boolean))).sort();
areasCacheRef.current.set(state, list); // cache it
setAreas(list);
} catch (err) {
if (err.name === 'AbortError') return; // superseded by a newer request
setAreas([]);
setAreasErr('Could not load Areas. You can proceed without selecting one.');
} finally {
if (inflightRef.current === controller) inflightRef.current = null;
setLoadingAreas(false);
}
}, 250); // 250ms debounce
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [state]);
return () => { controller.abort(); };
}, [state]);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 p-4">
@ -409,6 +363,7 @@ return (
</span>
</label>
<select
key={state + ':' + areas.length}
id="area"
className="w-full px-3 py-2 border rounded-md"
value={area}
@ -416,7 +371,7 @@ return (
disabled={loadingAreas}
>
<option value="">
{loadingAreas ? 'Loading Areas...' : 'Select Area (optional)'}
{loadingAreas ? 'Loading Areas...' : 'Select Area'}
</option>
{areas.map((a, i) => (
<option key={i} value={a}>{a}</option>

View File

@ -230,7 +230,7 @@ function getMonthlyInterestRate() {
} else if (interestStrategy === 'FLAT') {
// e.g. 6% annual => 0.5% per month
return flatAnnualRate / 12;
} else if (interestStrategy === 'MONTE_CARLO') {
} else if (interestStrategy === 'RANDOM') {
// if using a random range or historical sample
if (monthlyReturnSamples.length > 0) {
const idx = Math.floor(Math.random() * monthlyReturnSamples.length);

View File

@ -3,10 +3,7 @@ import fetch from "node-fetch";
import dotenv from "dotenv";
// Load environment variables
const envFile = process.env.NODE_ENV === "production" ? ".env.production" : ".env.development";
dotenv.config({ path: envFile });
console.log(`🛠️ Loaded environment variables from ${envFile}`);
const envFile = process.env.ENV_NAME;
// O*Net API Credentials
const ONET_USERNAME = process.env.ONET_USERNAME;

View File

@ -1,21 +1,6 @@
{
"status": "failed",
"failedTests": [
"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"
"b3209788e3afa146abdb-62296b89ebc040de2358"
]
}

View File

@ -0,0 +1,58 @@
# 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 "Verify your account" [level=1] [ref=e25]
- paragraph [ref=e26]: You must verify before using AptivaAI.
- generic [ref=e27]:
- heading "Email verification" [level=2] [ref=e28]
- generic [ref=e29]:
- button "Send email" [ref=e30] [cursor=pointer]
- textbox "Paste token" [ref=e31]
- button "Confirm" [ref=e32] [cursor=pointer]
- generic [ref=e33]:
- heading "Phone verification (optional)" [level=2] [ref=e34]
- generic [ref=e35]:
- checkbox "By requesting a code, you agree to receive one-time texts from AptivaAI for account verification and security alerts. Message frequency varies. Msg & data rates may apply. Reply STOP to opt out, HELP for help. Consent is not a condition of purchase or service. See the SMS Terms, Privacy Policy, and Terms." [ref=e36]
- generic [ref=e37]:
- text: By requesting a code, you agree to receive one-time texts from AptivaAI for account verification and security alerts. Message frequency varies. Msg & data rates may apply. Reply
- strong [ref=e38]: STOP
- text: to opt out,
- strong [ref=e39]: HELP
- text: for help. Consent is not a condition of purchase or service. See the
- link "SMS Terms" [ref=e40] [cursor=pointer]:
- /url: /sms
- text: ","
- link "Privacy Policy" [ref=e41] [cursor=pointer]:
- /url: /legal/privacy
- text: ", and"
- link "Terms" [ref=e42] [cursor=pointer]:
- /url: /legal/terms
- text: .
- generic [ref=e43]:
- textbox "+1XXXXXXXXXX" [ref=e44]: "1"
- button "Send code" [disabled] [ref=e45]
- generic [ref=e46]:
- textbox "6-digit code" [ref=e47]
- button "Confirm" [disabled] [ref=e48]
- button "Open chat" [ref=e49] [cursor=pointer]:
- img [ref=e50] [cursor=pointer]
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

15
tests/.auth/state.json Normal file
View File

@ -0,0 +1,15 @@
{
"cookies": [
{
"name": "aptiva_session",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ODgsImlhdCI6MTc1Nzk1Mjc1MiwiZXhwIjoxNzU3OTU5OTUyfQ.U54cm5RZ-n06sRoSAODykl1CGfKv5GPppxTt2jxuowM",
"domain": "dev1.aptivaai.com",
"path": "/",
"expires": 1757959952.858825,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
}
],
"origins": []
}

View File

@ -8,7 +8,7 @@ function uniq() {
}
test.describe('@p0 SignUp → Journey select → Route', () => {
test.setTimeout(10000); // allow for slower first load + areas fetch
test.setTimeout(12000); // allow for slower first load + areas fetch
test('create a new user via UI and persist creds for later specs', async ({ page }) => {
const u = uniq();
@ -100,6 +100,7 @@ test.describe('@p0 SignUp → Journey select → Route', () => {
expect(cookies.some((c) => /jwt|session/i.test(c.name))).toBeTruthy();
// No console errors
/** @type {string[]} */
const errors = [];
page.on('console', (m) => {
if (m.type() === 'error') errors.push(m.text());

View File

@ -18,19 +18,57 @@ test.describe('@p0 SignIn → Landing', () => {
await page.getByPlaceholder('Password', { exact: true }).fill(user.password);
await page.getByRole('button', { name: /^Sign In$/ }).click();
await page.waitForURL('**/signin-landing**', { timeout: 15000 });
await expect(
page.getByRole('heading', { name: new RegExp(`Welcome to AptivaAI\\s+${user.firstname}!`) })
).toBeVisible();
// Wait explicitly for the /api/signin POST and capture the outcome
const signinResp = await page.waitForResponse(
r => r.url().includes('/api/signin') && r.request().method() === 'POST',
{ timeout: 15000 }
);
const status = signinResp.status();
let bodyText = '';
try { bodyText = await signinResp.text(); } catch {}
await expect(page.getByRole('link', { name: /Go to Exploring/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Go to Preparing/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Go to Enhancing/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Go to Retirement/i })).toBeVisible();
// If backend rejected signin, fail here with clear diagnostics
expect(status, `POST /api/signin → ${status}\n${bodyText}`).toBeLessThan(400);
// Authenticated redirect can go to /verify (new gate) OR /signin-landing (legacy) OR journey.
await page.waitForLoadState('networkidle');
const url = page.url();
if (url.includes('/verify')) {
// Complete email verification via existing API (token exposed by server in non-prod).
await expect(page.getByText(/Verify your account/i)).toBeVisible({ timeout: 10000 });
const resp = await page.request.post('/api/auth/verify/email/send', { data: {} });
expect(resp.status()).toBeLessThan(400);
const json = await resp.json();
// If server is prod-like and doesnt expose test_token, fail fast with diagnostics.
expect(json.test_token, 'Server did not expose test_token (non-production only)').toBeTruthy();
// Confirm directly via API to avoid timing on auto-redirect.
const confirm = await page.request.post('/api/auth/verify/email/confirm', {
data: { token: json.test_token }
});
expect(confirm.status()).toBeLessThan(400);
// Navigate to the authenticated home now that VerificationGate will pass.
await page.goto('/signin-landing', { waitUntil: 'networkidle' });
await expect(page.getByText(/Welcome to AptivaAI/i)).toBeVisible({ timeout: 10000 });
await expect(page.getByRole('link', { name: /Go to Exploring/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Go to Preparing/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Go to Enhancing/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Go to Retirement/i })).toBeVisible();
// continue below to cookie + console checks
} else if (url.includes('/signin-landing')) {
// Greeting is not personalized here; accept any "Welcome to AptivaAI" variant.
await expect(page.getByText(/Welcome to AptivaAI/i)).toBeVisible({ timeout: 5000 });
await expect(page.getByRole('link', { name: /Go to Exploring/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Go to Preparing/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Go to Enhancing/i })).toBeVisible();
await expect(page.getByRole('link', { name: /Go to Retirement/i })).toBeVisible();
} else {
// Journey-resume path: just prove were authenticated and not stuck on /signin.
expect(url).not.toMatch(/\/signin($|[?#])/);
}
const cookies = await page.context().cookies();
expect(cookies.some(c => /jwt|session/i.test(c.name))).toBeTruthy();
/** @type {string[]} */
const consoleErrors = [];
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
expect(consoleErrors).toEqual([]);

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,5 +1,5 @@
// tests/e2e/45b-financial-profile.spec.mjs
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,4 +1,4 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -1,5 +1,5 @@
// @ts-check
// @ts-nocheck
import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';

View File

@ -0,0 +1,32 @@
// Real-UI login once, save storage state for all tests (per worker contexts will load it).
// Uses existing test user loader and the same selectors your signin spec uses.
import { chromium } from '@playwright/test';
import fs from 'fs';
import path from 'path';
import { loadTestUser } from '../utils/testUser.js';
const STATE_PATH = '/home/jcoakley/aptiva-dev1-app/tests/.auth/state.json';
export default async () => {
const user = loadTestUser();
const baseURL = process.env.PW_BASE_URL || 'http://localhost:3000';
// Ensure target dir exists
fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true });
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// Real UI flow (selectors match your 02-signin-landing.spec.mjs)
await page.goto(new URL('/signin', baseURL).toString(), { waitUntil: 'networkidle' });
await page.getByPlaceholder('Username', { exact: true }).fill(user.username);
await page.getByPlaceholder('Password', { exact: true }).fill(user.password);
await page.getByRole('button', { name: /^Sign In$/ }).click();
await page.waitForURL('**/signin-landing**', { timeout: 30000 });
// Persist authenticated cookies/localStorage
await context.storageState({ path: STATE_PATH });
await browser.close();
};