HR-ATS-Portal/frontend/candidate-profile.test.mjs

199 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* 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 (3201536px)')
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() }