From 0118254046e0e6c32ce24ea7e97246363a20d516 Mon Sep 17 00:00:00 2001 From: Matthew Jackson Date: Fri, 6 Mar 2026 21:11:50 -0800 Subject: [PATCH] Poll for submission success instead of single check After clicking submit, poll up to 4 times (2.5s + 3x2s = ~8.5s total) for success text or form disappearance. Handles invisible reCAPTCHA verification delay. Stops early on validation errors. Co-Authored-By: Claude Opus 4.6 --- lib/apply/ashby.mjs | 7 +++++-- lib/apply/generic.mjs | 44 ++++++++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/lib/apply/ashby.mjs b/lib/apply/ashby.mjs index b3b5525..1e0b3ec 100644 --- a/lib/apply/ashby.mjs +++ b/lib/apply/ashby.mjs @@ -20,8 +20,11 @@ export async function apply(page, job, formFiller) { await page.$('input[type="file"][name*="resume"]') || await page.$('input[type="file"]'); if (fileInput) { - await fileInput.setInputFiles(formFiller.profile.resume_path).catch(() => {}); - await page.waitForTimeout(1500); + const hasFile = await fileInput.evaluate(el => !!el.value); + if (!hasFile) { + await fileInput.setInputFiles(formFiller.profile.resume_path).catch(() => {}); + await page.waitForTimeout(1500); + } } }, }); diff --git a/lib/apply/generic.mjs b/lib/apply/generic.mjs index cbe4187..995b253 100644 --- a/lib/apply/generic.mjs +++ b/lib/apply/generic.mjs @@ -149,28 +149,34 @@ async function fillAndSubmit(page, job, formFiller, meta, opts) { if (hasSubmit) { await submitBtn.click(); - await page.waitForTimeout(SUBMIT_WAIT); + // Wait for submission — poll for success or form disappearance + // (invisible reCAPTCHA + server round-trip can take several seconds) const verifySelector = opts.verifySelector || 'form button[type="submit"]:not([disabled])'; - const postSubmit = await page.evaluate((vs) => { - const text = (document.body.innerText || '').toLowerCase(); - // Check for validation errors (red text, error messages) - const errorEls = document.querySelectorAll('[class*="error"], [class*="invalid"], [role="alert"]'); - const validationErrors = Array.from(errorEls) - .map(el => el.innerText?.trim()) - .filter(t => t && t.length < 200) - .slice(0, 3); - return { - hasSuccess: text.includes('application submitted') || text.includes('successfully applied') || - text.includes('thank you') || text.includes('application received') || - text.includes('application has been') || text.includes('we received your'), - hasForm: !!document.querySelector(vs), - validationErrors, - }; - }, verifySelector).catch(() => ({ hasSuccess: false, hasForm: false, validationErrors: [] })); + let postSubmit = { hasSuccess: false, hasForm: true, validationErrors: [] }; + for (let wait = 0; wait < 4; wait++) { + await page.waitForTimeout(wait === 0 ? SUBMIT_WAIT : 2000); + postSubmit = await page.evaluate((vs) => { + const text = (document.body.innerText || '').toLowerCase(); + const errorEls = document.querySelectorAll('[class*="error"], [class*="invalid"], [role="alert"]'); + const validationErrors = Array.from(errorEls) + .map(el => el.innerText?.trim()) + .filter(t => t && t.length < 200) + .slice(0, 3); + return { + hasSuccess: text.includes('application submitted') || text.includes('successfully applied') || + text.includes('thank you') || text.includes('application received') || + text.includes('application has been') || text.includes('we received your'), + hasForm: !!document.querySelector(vs), + validationErrors, + }; + }, verifySelector).catch(() => ({ hasSuccess: false, hasForm: false, validationErrors: [] })); - if (postSubmit.hasSuccess || !postSubmit.hasForm) { - return { status: 'submitted', meta }; + if (postSubmit.hasSuccess || !postSubmit.hasForm) { + return { status: 'submitted', meta }; + } + // Stop polling if validation errors appeared (form didn't submit) + if (postSubmit.validationErrors.length > 0) break; } return {