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 <noreply@anthropic.com>
This commit is contained in:
@@ -14,15 +14,65 @@ export async function apply(page, job, formFiller) {
|
|||||||
submitSelector: 'button:has-text("Submit Application")',
|
submitSelector: 'button:has-text("Submit Application")',
|
||||||
verifySelector: '#_systemfield_name',
|
verifySelector: '#_systemfield_name',
|
||||||
beforeSubmit: async (page, formFiller) => {
|
beforeSubmit: async (page, formFiller) => {
|
||||||
if (!formFiller.profile.resume_path) return;
|
// Upload resume — #_systemfield_resume IS the file input (not a container)
|
||||||
// #_systemfield_resume IS the file input (not a container)
|
if (formFiller.profile.resume_path) {
|
||||||
const fileInput = await page.$('input[type="file"]#_systemfield_resume') ||
|
const fileInput = await page.$('input[type="file"]#_systemfield_resume') ||
|
||||||
await page.$('input[type="file"]');
|
await page.$('input[type="file"]');
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
const hasFile = await fileInput.evaluate(el => !!el.value);
|
const hasFile = await fileInput.evaluate(el => !!el.value);
|
||||||
if (!hasFile) {
|
if (!hasFile) {
|
||||||
await fileInput.setInputFiles(formFiller.profile.resume_path).catch(() => {});
|
await fileInput.setInputFiles(formFiller.profile.resume_path).catch(() => {});
|
||||||
await page.waitForTimeout(2000);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,13 +14,42 @@ export async function apply(page, job, formFiller) {
|
|||||||
submitSelector: 'button:has-text("Submit application"), input[type="submit"]',
|
submitSelector: 'button:has-text("Submit application"), input[type="submit"]',
|
||||||
verifySelector: '#application-form',
|
verifySelector: '#application-form',
|
||||||
beforeSubmit: async (page, formFiller) => {
|
beforeSubmit: async (page, formFiller) => {
|
||||||
if (!formFiller.profile.resume_path) return;
|
// Upload resume
|
||||||
const resumeInput = await page.$('#resume');
|
if (formFiller.profile.resume_path) {
|
||||||
if (resumeInput) {
|
const resumeInput = await page.$('#resume');
|
||||||
const hasFile = await resumeInput.evaluate(el => !!el.value);
|
if (resumeInput) {
|
||||||
if (!hasFile) {
|
const hasFile = await resumeInput.evaluate(el => !!el.value);
|
||||||
await resumeInput.setInputFiles(formFiller.profile.resume_path).catch(() => {});
|
if (!hasFile) {
|
||||||
await page.waitForTimeout(1000);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user