From c38f80086f0dbdcda3e75b12d82ddd8167832a13 Mon Sep 17 00:00:00 2001 From: Matthew Jackson Date: Fri, 6 Mar 2026 21:32:49 -0800 Subject: [PATCH] Handle Ashby button-style questions and Greenhouse React Selects Ashby: detect Yes/No button questions for work auth, sponsorship, and consent. Click appropriate button in beforeSubmit hook. Greenhouse: use pressSequentially instead of fill() for React Select comboboxes (Country, Location). Click the dropdown option after typing. Co-Authored-By: Claude Opus 4.6 --- lib/apply/ashby.mjs | 68 ++++++++++++++++++++++++++++++++++------ lib/apply/greenhouse.mjs | 43 ++++++++++++++++++++----- 2 files changed, 95 insertions(+), 16 deletions(-) diff --git a/lib/apply/ashby.mjs b/lib/apply/ashby.mjs index 0650754..d50226c 100644 --- a/lib/apply/ashby.mjs +++ b/lib/apply/ashby.mjs @@ -14,15 +14,65 @@ export async function apply(page, job, formFiller) { submitSelector: 'button:has-text("Submit Application")', verifySelector: '#_systemfield_name', beforeSubmit: async (page, formFiller) => { - if (!formFiller.profile.resume_path) return; - // #_systemfield_resume IS the file input (not a container) - const fileInput = await page.$('input[type="file"]#_systemfield_resume') || - await page.$('input[type="file"]'); - if (fileInput) { - const hasFile = await fileInput.evaluate(el => !!el.value); - if (!hasFile) { - await fileInput.setInputFiles(formFiller.profile.resume_path).catch(() => {}); - await page.waitForTimeout(2000); + // Upload resume — #_systemfield_resume IS the file input (not a container) + if (formFiller.profile.resume_path) { + const fileInput = await page.$('input[type="file"]#_systemfield_resume') || + await page.$('input[type="file"]'); + if (fileInput) { + const hasFile = await fileInput.evaluate(el => !!el.value); + if (!hasFile) { + await fileInput.setInputFiles(formFiller.profile.resume_path).catch(() => {}); + await page.waitForTimeout(2000); + } + } + } + + // Ashby uses button-style Yes/No for questions (not radios/fieldsets). + // Find question labels and click the appropriate button. + const buttonQuestions = await page.evaluate(() => { + const questions = []; + // Find containers with question text + Yes/No buttons + const allBtns = Array.from(document.querySelectorAll('button[type="submit"]')); + const yesNoBtns = allBtns.filter(b => b.innerText.trim() === 'Yes' || b.innerText.trim() === 'No'); + const seen = new Set(); + for (const btn of yesNoBtns) { + const container = btn.parentElement?.parentElement; + if (!container || seen.has(container)) continue; + seen.add(container); + const label = container.innerText.replace(/Yes\s*No/g, '').trim(); + if (label) questions.push({ label, containerIdx: questions.length }); + } + return questions; + }); + + const p = formFiller.profile; + for (const q of buttonQuestions) { + const ll = q.label.toLowerCase(); + let answer = null; + if (ll.includes('authorized') || ll.includes('legally') || ll.includes('eligible') || ll.includes('right to work')) { + answer = p.work_authorization?.authorized ? 'Yes' : 'No'; + } else if (ll.includes('sponsor')) { + answer = p.work_authorization?.requires_sponsorship ? 'Yes' : 'No'; + } else if (ll.includes('consent') || ll.includes('text message') || ll.includes('sms')) { + answer = 'Yes'; + } + if (answer) { + // Click the matching button - find by text within the question's container + const clicked = await page.evaluate((label, answer) => { + const allBtns = Array.from(document.querySelectorAll('button[type="submit"]')); + const containers = new Set(); + for (const btn of allBtns) { + const c = btn.parentElement?.parentElement; + if (!c) continue; + const cText = c.innerText.replace(/Yes\s*No/g, '').trim(); + if (cText === label) { + const target = Array.from(c.querySelectorAll('button')).find(b => b.innerText.trim() === answer); + if (target) { target.click(); return true; } + } + } + return false; + }, q.label, answer); + if (clicked) await page.waitForTimeout(300); } } }, diff --git a/lib/apply/greenhouse.mjs b/lib/apply/greenhouse.mjs index 768168a..813bc7d 100644 --- a/lib/apply/greenhouse.mjs +++ b/lib/apply/greenhouse.mjs @@ -14,13 +14,42 @@ export async function apply(page, job, formFiller) { 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); + // Upload resume + if (formFiller.profile.resume_path) { + 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); + } + } + } + + // Fix React Select dropdowns (Country, Location) — fill() doesn't trigger them + const reactSelects = [ + { id: 'country', value: formFiller.profile.location?.country || 'United States' }, + { id: 'candidate-location', value: `${formFiller.profile.location?.city || ''}, ${formFiller.profile.location?.state || ''}` }, + ]; + for (const { id, value } of reactSelects) { + const el = await page.$(`#${id}`); + if (!el) continue; + const currentVal = await el.evaluate(e => e.value); + // Check if React Select already has a selection + const hasSelection = await page.evaluate((inputId) => { + const singleVal = document.querySelector(`#${inputId}`)?.closest('[class*="select__"]')?.querySelector('[class*="singleValue"]'); + return !!singleVal; + }, id); + if (hasSelection) continue; + + await el.click(); + await el.evaluate(e => { e.value = ''; }); + await el.pressSequentially(value, { delay: 30 }); + await page.waitForTimeout(1000); + const option = await page.$(`[id*="react-select-${id}-option"]`); + if (option) { + await option.click(); + await page.waitForTimeout(300); } } },