/* Candidate workspace integration + responsive checks. Start Vite first, then: node candidate-profile.test.mjs ATS_BASE_URL / CHROME_PATH override the local server/browser. All API calls are intercepted with fixtures; this test never writes to the real backend. ATS_SCREENSHOT_DIR optionally saves desktop and mobile preview images. */ import assert from 'node:assert/strict' import { existsSync, mkdirSync } from 'node:fs' import { join } from 'node:path' import puppeteer from 'puppeteer-core' const base = process.env.ATS_BASE_URL || 'http://127.0.0.1:5173' const chrome = process.env.CHROME_PATH || [ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/usr/bin/google-chrome', '/usr/bin/chromium', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', ].find(existsSync) assert(chrome, 'Set CHROME_PATH to an installed Chrome browser') const permissions = ['dashboard', 'inbox', 'jobs', 'candidates', 'pipeline', 'interviews', 'settings', 'requisitions'].flatMap((module) => ['view', 'create', 'edit', 'manage'].map((action) => `${module}.${action}`)) const user = { id: 'test-recruiter', name: 'Adeel Haider', email: 'adeel@example.com', role_name: 'hr_administrator', permissions } const fixture = { user_id: 'test-candidate', inbox_id: 101, message_id: 'test-message', name: 'Sarah Khan', email: 'sarah.khan@example.com', phone: '+92 300 0000000', city: 'Lahore, Pakistan', currentCompany: 'Techvista Solutions', current_title: 'Digital Marketing Specialist', experience: '5 years', education: 'BBA - Marketing, LUMS', linkedin_url: 'https://example.com/sarah', job_title: 'Marketing Manager', assigned_job_post_id: 'job-marketing', source: 'LinkedIn', application_status: 'PENDING', applied: '2026-09-09T09:00:00Z', rating: 4, favorite: false, professional_summary: 'Results-driven digital marketing professional with 5 years of experience in developing and executing data-driven marketing strategies. Experienced in increasing brand visibility, improving customer engagement, and delivering measurable growth through SEO, PPC, and social media campaigns.', matched_keywords: ['Digital Marketing', 'SEO', 'Google Ads', 'Social Media', 'Content Marketing', 'Analytics', 'Brand Strategy'], recruiter: 'Adeel Haider', documents: [ { name: 'Sarah_Khan_Resume.pdf', path: 'Email/test/resume.pdf' }, { name: 'Portfolio.pdf', path: 'Email/test/portfolio.pdf' }, { name: 'Cover_Letter.pdf', path: 'Email/test/cover.pdf' }, ], previous_applications: [ { source: 'inbox', inbox_id: 101, message_id: 'test-message', job_post_id: 'job-marketing', job_title: 'Marketing Manager', status: 'PENDING', applied_at: '2026-09-09' }, { source: 'inbox', inbox_id: 99, message_id: 'old-message', job_post_id: 'job-social', job_title: 'Social Media Specialist', status: 'CLOSED', applied_at: '2026-08-03' }, { source: 'manual', manual_upload_candidate_id: 'manual-old', job_title: 'Digital Marketing Executive', status: 'REJECTED', applied_at: '2026-07-20' }, ], notes: [{ id: 'note-1', note: 'Relevant paid-media experience. Explore budget ownership during screening.', created_by_name: 'Adeel Haider', created_at: '2026-09-09T11:30:00Z' }], interviews: [{ id: 'interview-1', interview_type: 'Phone Screen', interview_date: '2026-09-11T09:00:00Z', interview_status: 'Scheduled' }], activity: [{ id: 'activity-1', activity_type: 'Recruiter assigned', description: 'Adeel Haider is responsible for the current application.', activity_date: '2026-09-10T09:00:00Z' }], } const browser = await puppeteer.launch({ executablePath: chrome, headless: true, args: ['--no-sandbox'], defaultViewport: { width: 1536, height: 1100 } }) const page = await browser.newPage() const errors = [] const writes = [] const reads = [] let candidate = structuredClone(fixture) let activeUser = user let failDetail = false page.on('pageerror', (error) => errors.push(error.message)) await page.setRequestInterception(true) page.on('request', async (request) => { if (!['fetch', 'xhr', 'preflight'].includes(request.resourceType()) && new URL(request.url()).port !== '8000') return request.continue() const path = new URL(request.url()).pathname reads.push({ path, url: request.url() }) let data = [] if (request.method() === 'OPTIONS') return request.respond({ status: 204, headers: { 'access-control-allow-origin': '*', 'access-control-allow-headers': '*', 'access-control-allow-methods': '*' } }) if (path === '/users/me') data = activeUser if (path === '/candidate/fetch') { const userId = new URL(request.url()).searchParams.get('user_id') if (!userId) data = [] else if (userId === 'test-candidate-b') data = { ...candidate, user_id: 'test-candidate-b', name: 'Omar Ali' } else if (userId === 'test-candidate-c') data = { ...candidate, user_id: 'test-candidate-c', name: 'Hina Raza' } else data = candidate } if (path === '/forms/definitions') data = {} if (path === '/s3/open') data = { url: `${base}/fixture-resume.pdf` } if (path === '/fixture-resume.pdf') return request.respond({ status: 200, contentType: 'application/pdf', body: '%PDF-1.4\n%%EOF' }) if (request.method() !== 'GET') { const body = JSON.parse(request.postData() || '{}') writes.push({ path, body }) if (path === '/candidate/update') Object.assign(candidate, body) if (path === '/candidate/stage') candidate.application_status = body.to_stage if (path === '/notes/create') candidate.notes.push({ id: 'new-note', note: body.note, created_at: new Date().toISOString() }) } return request.respond({ status: path === '/candidate/fetch' && failDetail ? 500 : 200, contentType: 'application/json', headers: { 'access-control-allow-origin': '*' }, body: JSON.stringify({ data, status_code: 200 }) }) }) await page.evaluateOnNewDocument((account) => { localStorage.setItem('tf-auth', JSON.stringify({ access_token: 'fixture-token', refresh_token: 'fixture-refresh', expires_at: Date.now() + 3600000, data: account })) sessionStorage.setItem('tf-candidate-browse', JSON.stringify({ entries: [ { userId: 'test-candidate', name: 'Sarah Khan', stage: 'Shortlist', jobTitle: 'Marketing Manager' }, { userId: 'test-candidate-b', name: 'Omar Ali', stage: 'Interview', jobTitle: 'Marketing Manager' }, { userId: 'test-candidate-c', name: 'Hina Raza', stage: 'Screening', jobTitle: 'Content Lead' }, ], })) }, user) async function clickText(selector, text) { const clicked = await page.evaluate((selector, text) => { const button = [...document.querySelectorAll(selector)].find((item) => item.textContent.trim() === text) if (!button) return false button.click(); return true }, selector, text) assert(clicked, `Missing ${text}`) } async function load() { await page.goto(`${base}/candidate/test-candidate`, { waitUntil: 'networkidle0' }) await page.waitForSelector('.cw-hero h1') } try { await load() assert.equal(await page.$eval('.cw-hero h1', (element) => element.textContent), 'Sarah Khan') assert.equal(await page.$$eval('.cw-applications tbody tr', (rows) => rows.length), 3) assert.ok(await page.$('.sidebar'), 'Candidate page keeps the app sidebar') assert.equal(await page.$eval('.cw-browse-counter', (element) => element.textContent.trim()), '1 of 3') assert.equal(writes.length, 0, 'Opening a candidate is read-only') for (const width of [1536, 1280, 1024, 768, 390, 320]) { await page.setViewport({ width, height: 1100 }) const overflow = await page.evaluate(() => [document.documentElement, document.querySelector('.content'), document.querySelector('.cand-page')].map((node) => node.scrollWidth - node.clientWidth)) assert(overflow.every((amount) => amount <= 1), `Overflow at ${width}px: ${overflow}`) if (process.env.ATS_SCREENSHOT_DIR && [1536, 390].includes(width)) { mkdirSync(process.env.ATS_SCREENSHOT_DIR, { recursive: true }) await page.screenshot({ path: join(process.env.ATS_SCREENSHOT_DIR, `candidate-${width}.png`), fullPage: true }) } } console.log('ok Profile data, 3-column desktop and mobile layouts (320–1536px)') await page.setViewport({ width: 1536, height: 1100 }) await page.click('[aria-label="Next candidate"]') await page.waitForFunction(() => document.querySelector('.cw-hero h1')?.textContent === 'Omar Ali') assert.equal(await page.$eval('.cw-browse-counter', (element) => element.textContent.trim()), '2 of 3') assert.match(page.url(), /\/candidate\/test-candidate-b/) await page.click('[aria-label="Previous candidate"]') await page.waitForFunction(() => document.querySelector('.cw-hero h1')?.textContent === 'Sarah Khan') console.log('ok Previous / next walks the candidate list') await clickText('.cand-page-actions button', 'Favorite') await page.waitForFunction(() => document.querySelector('.cand-page-actions button').getAttribute('aria-pressed') === 'true') await page.click('.cw-rating [role=radio]:nth-child(5)') await page.waitForFunction(() => document.querySelector('.cw-rating').textContent.includes('5.0 / 5')) assert(writes.some((write) => write.path === '/candidate/update' && write.body.rating === 5)) await clickText('.cw-action-grid button', 'Add Note') await page.type('.candidate-dialog textarea', 'Follow up on campaign results.') await clickText('.candidate-dialog button', 'Add Note') await page.waitForSelector('.candidate-dialog', { hidden: true }) assert(writes.some((write) => write.path === '/notes/create' && write.body.note === 'Follow up on campaign results.')) console.log('ok Favorite, rating and notes retain existing API requests') await clickText('.cw-action-grid button', 'Schedule Interview') await page.$eval('.candidate-dialog input[type=date]', (input) => { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set setter.call(input, '2027-01-20'); input.dispatchEvent(new Event('input', { bubbles: true })) }) await clickText('.candidate-dialog button', 'Schedule Interview') await page.waitForSelector('.candidate-dialog', { hidden: true }) assert(writes.some((write) => write.path === '/interview/create' && write.body.inbox_id === 101)) const downloadSession = await page.createCDPSession() await downloadSession.send('Page.setDownloadBehavior', { behavior: 'deny' }) await clickText('.cw-hero-actions button', 'Download CV') await page.waitForFunction(() => !document.querySelector('.cw-hero-actions button').disabled) assert(reads.some((read) => read.path === '/s3/open' && new URL(read.url).searchParams.get('key') === 'Email/test/resume.pdf')) assert(reads.some((read) => read.path === '/fixture-resume.pdf')) assert(!reads.some((read) => read.path === '/documents/download'), 'S3 documents must not use the local-file download endpoint') console.log('ok Interview scheduling and signed resume download') await page.select('.cw-status-grid select', 'Interview') const beforeCancel = writes.length await clickText('.candidate-dialog button', 'Cancel') assert.equal(writes.length, beforeCancel, 'Cancelling must not update stage') await page.select('.cw-status-grid select', 'Interview') await clickText('.candidate-dialog button', 'Save Stage') await page.waitForSelector('.candidate-dialog', { hidden: true }) assert(writes.some((write) => write.path === '/candidate/stage' && write.body.to_stage === 'INTERVIEW')) await clickText('.cw-action-grid button', 'Reject') assert(await page.$eval('.candidate-dialog button[type=submit]', (button) => button.disabled), 'Rejection needs a reason') await clickText('.candidate-dialog button', 'Cancel') console.log('ok Stage confirmation, cancellation, rejection reason') for (const tab of ['Resume', 'Interviews', 'Forms', 'Notes', 'Activity', 'Timeline', 'History', 'Overview']) { await page.evaluate((label) => [...document.querySelectorAll('[role=tab]')].find((item) => item.textContent.startsWith(label)).click(), tab) await page.waitForFunction((label) => document.querySelector('[role=tab][aria-selected=true]')?.textContent.startsWith(label), {}, tab) } await page.click('[role=tab][aria-selected=true]') await page.keyboard.press('ArrowRight') assert((await page.$eval('[role=tab][aria-selected=true]', (tab) => tab.textContent)).startsWith('Resume')) assert(await page.$eval('[role=tabpanel]', (panel) => Boolean(document.getElementById(panel.getAttribute('aria-labelledby'))))) console.log('ok All tabs and keyboard navigation') activeUser = { ...user, role_name: 'hiring_manager' } await load() assert.deepEqual(await page.$$eval('[role=tab]', (tabs) => tabs.map((tab) => tab.textContent.replace(/\d/g, '').trim())), ['Forms', 'Notes']) assert.equal(await page.$('.cw-overview'), null) console.log('ok Hiring manager restricted view') activeUser = { ...user, permissions: ['candidates.view', 'interviews.view'] } candidate = { user_id: 'test-candidate', name: 'Candidate with no attachments', notes: [], interviews: [], documents: [] } await load() assert.equal(await page.$('.cw-document'), null) assert(await page.$eval('.cand-page-actions button', (button) => button.disabled)) assert(await page.$eval('.cw-status-grid select', (select) => select.disabled)) assert((await page.$eval('.cw-overview', (element) => element.textContent)).includes('No resume attached')) console.log('ok Missing fields, missing attachments and read-only permissions') activeUser = user failDetail = true await load() await page.waitForFunction(() => document.querySelector('.cand-page').textContent.includes('Could not load this candidate'), { timeout: 15000 }) assert.equal(await page.$('.cw-overview'), null, 'Do not show stale candidate details after a failed load') failDetail = false await clickText('.cand-page button', 'Try again') await page.waitForSelector('.cw-overview') console.log('ok Load failure and retry') assert.deepEqual(errors, [], 'No runtime errors') console.log('All candidate workspace checks passed') } finally { await browser.close() }