/** * generic.mjs — Generic external ATS handler * Best-effort form filler for any career page with a standard HTML form. * Handles single-page and multi-step flows (up to 5 steps). * Skips pages that require account creation or have CAPTCHAs. */ import { NAVIGATION_TIMEOUT, PAGE_LOAD_WAIT, FORM_FILL_WAIT, SUBMIT_WAIT } from '../constants.mjs'; export const SUPPORTED_TYPES = ['unknown_external']; const MAX_STEPS = 5; export async function apply(page, job, formFiller) { const url = job.apply_url; if (!url) return { status: 'no_button', meta: { title: job.title, company: job.company } }; await page.goto(url, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT }); await page.waitForTimeout(PAGE_LOAD_WAIT); const meta = await page.evaluate(() => ({ title: document.querySelector('h1')?.textContent?.trim()?.slice(0, 100), company: document.querySelector('[class*="company"] h2, h2, [class*="employer"]')?.textContent?.trim()?.slice(0, 80), })).catch(() => ({})); meta.title = meta.title || job.title; meta.company = meta.company || job.company; // Detect blockers: login walls, CAPTCHAs, closed listings const pageCheck = await page.evaluate(() => { const text = (document.body.innerText || '').toLowerCase(); const hasLogin = !!(document.querySelector('input[type="password"]') || (text.includes('sign in') && text.includes('create account')) || (text.includes('log in') && text.includes('register'))); // Only block on visible CAPTCHAs — invisible reCAPTCHA (size=invisible) fires on submit and usually passes const captchaFrames = Array.from(document.querySelectorAll('iframe[src*="recaptcha"], iframe[src*="captcha"]')); const hasVisibleCaptcha = captchaFrames.some(f => { if (f.src.includes('size=invisible')) return false; const rect = f.getBoundingClientRect(); return rect.width > 50 && rect.height > 50; }); const hasCaptcha = hasVisibleCaptcha; const isClosed = text.includes('no longer accepting') || text.includes('position has been filled') || text.includes('this job is no longer') || text.includes('job not found') || text.includes('this position is closed') || text.includes('listing has expired') || text.includes('no longer available') || text.includes('page you are looking for') || text.includes('job may be no longer') || text.includes('does not exist') || text.includes('this role has been filled') || text.includes('posting has closed') || document.title.toLowerCase().includes('404'); return { hasLogin, hasCaptcha, isClosed }; }).catch(() => ({})); if (pageCheck.isClosed) return { status: 'closed', meta }; if (pageCheck.hasLogin) return { status: 'skipped_login_required', meta }; if (pageCheck.hasCaptcha) return { status: 'skipped_captcha', meta }; // Some pages land directly on the form; others need an Apply button click // Check if we landed directly on a form (with or without