Reliability improvements: click retry, resume selection, answer loop, browser recovery
- Easy Apply click: click found element directly, retry with force if modal doesn't open - Resume: select first radio if none checked, fall back to file upload - AI answers: inject stored answers into formFiller on needs_answer retry - Answers persistence: reload answers.json before each job for Telegram replies - Browser recovery: detect dead page, create fresh browser session - Multiple dialogs: findApplyModal() tags the right dialog among cookie banners etc. - Select matching: case-insensitive fuzzy match with substring fallback - dismissModal: scope Discard scan to dialog elements only - Label dedup: normalize whitespace, fix odd-length edge case - no_modal status: explicit handleResult case - Per-job timeout: 10 minutes (was 3) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,30 @@ async function getModalDebugInfo(page, modalSelector) {
|
||||
return { heading, buttons, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Easy Apply modal among potentially multiple [role="dialog"] elements.
|
||||
* Returns the dialog that contains apply-related content (form, progress bar, submit button).
|
||||
* Falls back to the first dialog if none match specifically.
|
||||
*/
|
||||
async function findApplyModal(page) {
|
||||
const dialogs = await page.$$('[role="dialog"]');
|
||||
if (dialogs.length <= 1) return dialogs[0] || null;
|
||||
|
||||
// Multiple dialogs — find the one with apply content
|
||||
for (const d of dialogs) {
|
||||
const isApply = await d.evaluate(el => {
|
||||
const text = (el.innerText || '').toLowerCase();
|
||||
const hasForm = el.querySelector('form, input, select, textarea, fieldset') !== null;
|
||||
const hasProgress = el.querySelector('progress, [role="progressbar"]') !== null;
|
||||
const hasApplyHeading = /apply to\b/i.test(text);
|
||||
return hasForm || hasProgress || hasApplyHeading;
|
||||
}).catch(() => false);
|
||||
if (isApply) return d;
|
||||
}
|
||||
|
||||
return dialogs[0]; // fallback
|
||||
}
|
||||
|
||||
export async function apply(page, job, formFiller) {
|
||||
const meta = { title: job.title, company: job.company };
|
||||
|
||||
@@ -152,15 +176,39 @@ export async function apply(page, job, formFiller) {
|
||||
Object.assign(meta, pageMeta);
|
||||
|
||||
// Click Easy Apply and wait for modal to appear
|
||||
await page.click(LINKEDIN_APPLY_BUTTON_SELECTOR, { timeout: APPLY_CLICK_TIMEOUT }).catch(() => {});
|
||||
const modal = await page.waitForSelector(LINKEDIN_EASY_APPLY_MODAL_SELECTOR, { timeout: 8000 }).catch(() => null);
|
||||
// Click the actual found element — not a fresh selector query that might miss shadow DOM elements
|
||||
await eaBtn.click({ timeout: APPLY_CLICK_TIMEOUT }).catch(() => {});
|
||||
let modal = await page.waitForSelector(LINKEDIN_EASY_APPLY_MODAL_SELECTOR, { timeout: 8000 }).catch(() => null);
|
||||
|
||||
// Retry: button may not have been interactable on first click (lazy-loaded, overlapping element, etc.)
|
||||
if (!modal) {
|
||||
console.log(` ❌ Modal did not open after clicking Easy Apply`);
|
||||
console.log(` ℹ️ Modal didn't open — retrying click`);
|
||||
await page.evaluate(() => window.scrollTo(0, 0)).catch(() => {});
|
||||
await page.waitForTimeout(1000);
|
||||
// Re-find button in case DOM changed
|
||||
const eaBtn2 = await page.$(LINKEDIN_APPLY_BUTTON_SELECTOR) || eaBtn;
|
||||
await eaBtn2.click({ force: true, timeout: APPLY_CLICK_TIMEOUT }).catch(() => {});
|
||||
modal = await page.waitForSelector(LINKEDIN_EASY_APPLY_MODAL_SELECTOR, { timeout: 8000 }).catch(() => null);
|
||||
}
|
||||
|
||||
if (!modal) {
|
||||
console.log(` ❌ Modal did not open after clicking Easy Apply (2 attempts)`);
|
||||
console.log(` Action: LinkedIn may have changed the modal structure or login expired`);
|
||||
return { status: 'no_modal', meta };
|
||||
}
|
||||
|
||||
const MODAL = LINKEDIN_EASY_APPLY_MODAL_SELECTOR;
|
||||
// If multiple [role="dialog"] exist (cookie banners, notifications), tag the apply modal
|
||||
// so all subsequent selectors target the right one
|
||||
const applyModal = await findApplyModal(page);
|
||||
let MODAL = LINKEDIN_EASY_APPLY_MODAL_SELECTOR;
|
||||
if (applyModal) {
|
||||
const multipleDialogs = (await page.$$('[role="dialog"]')).length > 1;
|
||||
if (multipleDialogs) {
|
||||
await applyModal.evaluate(el => el.setAttribute('data-claw-apply-modal', 'true'));
|
||||
MODAL = '[data-claw-apply-modal="true"]';
|
||||
console.log(` ℹ️ Multiple dialogs detected — tagged apply modal`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step through modal
|
||||
let lastProgress = '-1';
|
||||
@@ -384,9 +432,9 @@ async function dismissModal(page, modalSelector) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: find Discard by text — scan all buttons via page.$$()
|
||||
const allBtns = await page.$$('button');
|
||||
for (const btn of allBtns) {
|
||||
// Fallback: find Discard by text — scope to dialogs/modals to avoid clicking wrong buttons
|
||||
const dialogBtns = await page.$$('[role="dialog"] button, [role="alertdialog"] button, [data-test-modal] button');
|
||||
for (const btn of dialogBtns) {
|
||||
const text = await btn.evaluate(el => (el.innerText || '').trim().toLowerCase()).catch(() => '');
|
||||
if (text === 'discard') {
|
||||
await btn.click().catch(() => {});
|
||||
|
||||
Reference in New Issue
Block a user