fix: stuck loop on unfilled selects, Continue button detection
- Stuck detection after clicking Next: if heading+progress unchanged 2x, exit with 'stuck' status instead of looping forever - Select dropdowns: treat "Select an option" as unfilled (LinkedIn's placeholder has a truthy value that was skipping the fill logic) - Continue button fallback: detect draft "Continue" span via apply URL pattern when Easy Apply button not found Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -112,7 +112,21 @@ export async function apply(page, job, formFiller) {
|
||||
|
||||
// Scroll slightly to trigger lazy-loaded content, then wait for Easy Apply button
|
||||
await page.evaluate(() => window.scrollTo(0, 300)).catch(() => {});
|
||||
const eaBtn = await page.waitForSelector(LINKEDIN_APPLY_BUTTON_SELECTOR, { timeout: 12000, state: 'attached' }).catch(() => null);
|
||||
let eaBtn = await page.waitForSelector(LINKEDIN_APPLY_BUTTON_SELECTOR, { timeout: 12000, state: 'attached' }).catch(() => null);
|
||||
|
||||
// Fallback: LinkedIn shows plain "Continue" when a draft exists (not a button — a span)
|
||||
// Look for it via the apply URL pattern in the page
|
||||
if (!eaBtn) {
|
||||
const continueEl = await page.$(`a[href*="/apply/"] span, [class*="apply"] span`);
|
||||
if (continueEl) {
|
||||
const text = await continueEl.evaluate(el => (el.innerText || '').trim()).catch(() => '');
|
||||
if (text === 'Continue') {
|
||||
eaBtn = continueEl;
|
||||
console.log(` ℹ️ Found "Continue" element (draft application)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!eaBtn) {
|
||||
console.log(` ℹ️ No Easy Apply button found. Page URL: ${page.url()}`);
|
||||
console.log(` Action: job may have been removed, filled, or changed to external apply`);
|
||||
@@ -139,6 +153,8 @@ export async function apply(page, job, formFiller) {
|
||||
|
||||
// Step through modal
|
||||
let lastProgress = '-1';
|
||||
let lastHeading = '';
|
||||
let samePageCount = 0;
|
||||
for (let step = 0; step < LINKEDIN_MAX_MODAL_STEPS; step++) {
|
||||
const modalStillOpen = await page.$(MODAL);
|
||||
if (!modalStillOpen) {
|
||||
@@ -203,7 +219,22 @@ export async function apply(page, job, formFiller) {
|
||||
console.log(` [step ${step}] clicking Next`);
|
||||
await nextBtn.click({ timeout: APPLY_CLICK_TIMEOUT }).catch(() => {});
|
||||
await page.waitForTimeout(CLICK_WAIT);
|
||||
|
||||
// Detect if we're stuck — same heading+progress means page didn't advance
|
||||
const curHeading = debugInfo.heading;
|
||||
if (curHeading === lastHeading && progress === lastProgress) {
|
||||
samePageCount++;
|
||||
if (samePageCount >= 2) {
|
||||
console.log(` [step ${step}] stuck — clicked Next but page didn't advance (${samePageCount} times)`);
|
||||
console.log(` Action: a required field may be unfilled. Check select dropdowns still at "Select an option"`);
|
||||
await dismissModal(page, MODAL);
|
||||
return { status: 'stuck', meta };
|
||||
}
|
||||
} else {
|
||||
samePageCount = 0;
|
||||
}
|
||||
lastProgress = progress;
|
||||
lastHeading = curHeading;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -217,6 +248,8 @@ export async function apply(page, job, formFiller) {
|
||||
await reviewBtn.click({ timeout: APPLY_CLICK_TIMEOUT }).catch(() => {});
|
||||
await page.waitForTimeout(CLICK_WAIT);
|
||||
lastProgress = progress;
|
||||
lastHeading = debugInfo.heading;
|
||||
samePageCount = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -244,12 +277,8 @@ export async function apply(page, job, formFiller) {
|
||||
return { status: 'incomplete', meta };
|
||||
}
|
||||
|
||||
// Stuck detection — progress hasn't changed and we've been through a few steps
|
||||
if (progress && progress === lastProgress && step > 2) {
|
||||
console.log(` [step ${step}] stuck — progress unchanged at ${progress}`);
|
||||
await dismissModal(page, MODAL);
|
||||
return { status: 'stuck', meta };
|
||||
}
|
||||
// Stuck detection — no Next/Review/Submit found
|
||||
// (stuck-after-click detection is handled above in the Next button section)
|
||||
|
||||
console.log(` [step ${step}] ❌ No Next/Review/Submit button found in modal`);
|
||||
console.log(` Action: LinkedIn may have changed button text/structure. Check button snapshot above.`);
|
||||
|
||||
@@ -285,7 +285,8 @@ export class FormFiller {
|
||||
if (!await sel.isVisible().catch(() => false)) continue;
|
||||
const lbl = await this.getLabel(sel);
|
||||
const existing = await sel.inputValue().catch(() => '');
|
||||
if (existing) continue;
|
||||
// "Select an option" is LinkedIn's placeholder — treat as unfilled
|
||||
if (existing && !/^select an? /i.test(existing)) continue;
|
||||
const answer = this.answerFor(lbl);
|
||||
if (answer) {
|
||||
await sel.selectOption({ label: answer }).catch(async () => {
|
||||
|
||||
Reference in New Issue
Block a user