diff --git a/.gitignore b/.gitignore index a4cba82..8e07ce0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,9 @@ config/search_config.json # Templates are committed instead (see config/*.example.json) data/*.lock +test_dry_run.mjs +test_full_flow.mjs +test_modal_diag.mjs +test_labels.mjs +test_buttons.mjs +test_submit.mjs diff --git a/lib/apply/easy_apply.mjs b/lib/apply/easy_apply.mjs index 114e6a3..14da4fe 100644 --- a/lib/apply/easy_apply.mjs +++ b/lib/apply/easy_apply.mjs @@ -273,20 +273,72 @@ export async function apply(page, job, formFiller) { if (submitBtn) { console.log(` [step ${step}] clicking Submit`); await submitBtn.click({ timeout: APPLY_CLICK_TIMEOUT }).catch(() => {}); - await page.waitForTimeout(SUBMIT_WAIT); - // Verify modal closed - const modalGone = !(await page.$(MODAL)); - if (modalGone) { + // Wait for modal to close — LinkedIn may take a few seconds after submit + const modalClosed = await page.waitForSelector(MODAL, { state: 'detached', timeout: 8000 }).then(() => true).catch(() => false); + if (modalClosed) { console.log(` ✅ Submit confirmed — modal closed`); return { status: 'submitted', meta }; } - // Modal still open — submit may have failed - console.log(` [step ${step}] ⚠️ Modal still open after Submit click`); - console.log(` Action: submit may have failed due to validation or network error`); - await dismissModal(page, MODAL); - return { status: 'incomplete', meta }; + // Modal still open — LinkedIn often shows a post-submit confirmation/success + // dialog that still matches [role="dialog"]. Check for success indicators. + const postSubmitModal = await page.$(MODAL); + if (postSubmitModal) { + const postSubmitInfo = await postSubmitModal.evaluate(el => { + const text = (el.innerText || el.textContent || '').toLowerCase(); + return { + hasSuccess: text.includes('application was sent') || text.includes('applied') || + text.includes('thank you') || text.includes('submitted') || + text.includes('application has been') || text.includes('successfully'), + hasDone: text.includes('done') || text.includes('got it'), + snippet: (el.innerText || '').trim().slice(0, 200), + }; + }).catch(() => ({ hasSuccess: false, hasDone: false, snippet: '' })); + + console.log(` [step ${step}] post-submit modal: "${postSubmitInfo.snippet}"`); + + if (postSubmitInfo.hasSuccess || postSubmitInfo.hasDone) { + console.log(` ✅ Submit confirmed — success dialog detected`); + // Try to dismiss the success dialog + const doneBtn = await findModalButton(page, MODAL, { + ariaLabels: ['Dismiss', 'Done', 'Close'], + exactTexts: ['Done', 'Got it', 'Close'], + }); + if (doneBtn) await doneBtn.click().catch(() => {}); + return { status: 'submitted', meta }; + } + + // Check for validation errors — real failure + const postErrors = await postSubmitModal.$$('[class*="error"], [aria-invalid="true"], .artdeco-inline-feedback--error'); + const errorTexts = []; + for (const e of postErrors) { + const t = await e.evaluate(el => el.textContent?.trim()?.slice(0, 80) || '').catch(() => ''); + if (t) errorTexts.push(t); + } + + if (errorTexts.length > 0) { + console.log(` [step ${step}] ❌ Validation errors after Submit: ${JSON.stringify(errorTexts)}`); + await dismissModal(page, MODAL); + return { status: 'incomplete', meta, validation_errors: errorTexts }; + } + + // No errors, no success text — but Submit button is gone, likely succeeded + // (LinkedIn sometimes shows a follow-up prompt like "Follow company?") + const submitStillThere = await findModalButton(page, MODAL, { + ariaLabels: ['Submit application'], + exactTexts: ['Submit application'], + }); + if (!submitStillThere) { + console.log(` ✅ Submit likely succeeded — Submit button gone, no errors`); + await dismissModal(page, MODAL); + return { status: 'submitted', meta }; + } + + console.log(` [step ${step}] ⚠️ Submit button still present — click may not have registered`); + await dismissModal(page, MODAL); + return { status: 'incomplete', meta }; + } } // Stuck detection — no Next/Review/Submit found