178 lines
12 KiB
JavaScript
178 lines
12 KiB
JavaScript
/* 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') 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 }))
|
||
}, 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.equal(await page.$('.sidebar'), null, 'Detail page must use full width')
|
||
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 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() }
|