diff --git a/lib/apply/easy_apply.mjs b/lib/apply/easy_apply.mjs index b9655b5..4573f69 100644 --- a/lib/apply/easy_apply.mjs +++ b/lib/apply/easy_apply.mjs @@ -212,7 +212,7 @@ export async function apply(page, job, formFiller) { } // Fill form fields — page.$() in form_filler pierces shadow DOM - const unknowns = await formFiller.fill(page, formFiller.profile.resume_path); + const unknowns = await formFiller.fill(page, formFiller.profile.resume_path, { modalSelector: MODAL }); if (unknowns.length > 0) console.log(` [step ${step}] unknown fields: ${JSON.stringify(unknowns.map(u => u.label || u))}`); if (unknowns[0]?.honeypot) { diff --git a/lib/apply/greenhouse.mjs b/lib/apply/greenhouse.mjs index 9204489..768168a 100644 --- a/lib/apply/greenhouse.mjs +++ b/lib/apply/greenhouse.mjs @@ -1,5 +1,8 @@ /** * greenhouse.mjs — Greenhouse ATS handler (extends generic) + * + * Greenhouse boards show the form directly on the page (no Apply button needed). + * Form ID: #application-form. Resume input: #resume. Submit: "Submit application". */ import { apply as genericApply } from './generic.mjs'; @@ -7,6 +10,19 @@ export const SUPPORTED_TYPES = ['greenhouse']; export async function apply(page, job, formFiller) { return genericApply(page, job, formFiller, { - submitSelector: 'button:has-text("Submit Application"), input[type="submit"]', + formDetector: '#application-form', + submitSelector: 'button:has-text("Submit application"), input[type="submit"]', + verifySelector: '#application-form', + beforeSubmit: async (page, formFiller) => { + if (!formFiller.profile.resume_path) return; + const resumeInput = await page.$('#resume'); + if (resumeInput) { + const hasFile = await resumeInput.evaluate(el => !!el.value); + if (!hasFile) { + await resumeInput.setInputFiles(formFiller.profile.resume_path).catch(() => {}); + await page.waitForTimeout(1000); + } + } + }, }); } diff --git a/lib/form_filler.mjs b/lib/form_filler.mjs index 60ba3c3..86148e2 100644 --- a/lib/form_filler.mjs +++ b/lib/form_filler.mjs @@ -10,7 +10,7 @@ import { writeFileSync, renameSync } from 'fs'; import { DEFAULT_YEARS_EXPERIENCE, DEFAULT_DESIRED_SALARY, MINIMUM_SALARY_FACTOR, DEFAULT_SKILL_RATING, - LINKEDIN_EASY_APPLY_MODAL_SELECTOR, FORM_PATTERN_MAX_LENGTH, + FORM_PATTERN_MAX_LENGTH, AUTOCOMPLETE_WAIT, AUTOCOMPLETE_TIMEOUT, ANTHROPIC_API_URL, DEFAULT_MODEL } from './constants.mjs'; @@ -553,9 +553,9 @@ Answer:`; * then only makes individual CDP calls for elements that need action. * Returns array of unknown required field labels. */ - async fill(page, resumePath) { + async fill(page, resumePath, { modalSelector = null } = {}) { const unknown = []; - const container = await page.$(LINKEDIN_EASY_APPLY_MODAL_SELECTOR) || page; + const container = modalSelector ? (await page.$(modalSelector) || page) : page; // Single DOM snapshot — all labels, values, visibility, required status const snap = await this._snapshotFields(container);