fix(connect): detect alertdialog/aria-modal and wait for late-mounting invite modal#458
Conversation
… modal The 2-button-gating fix in a48fccc was downstream of the actual failure: on accounts/profiles where the dialog opens correctly, the previous ``_dialog_is_open`` was returning False before the dialog had a chance to mount, so the secondary-button click that fix targeted never ran. Two issues, both rooted in `_DIALOG_SELECTOR` / `_dialog_is_open`: 1. **Selector was too narrow.** The "Add a note to your invitation?" gating dialog (and likely the inner artdeco-modal that hosts the note textarea) is rendered as ``role="alertdialog"`` with ``aria-modal="true"`` — neither matched ``dialog[open], [role="dialog"]``. Trace screenshots from a real failing run (``run-8fic76qw``) show the dialog visibly mounted at the same DOM moment ``_dialog_is_open`` was returning False. 2. **Race against modal mount.** ``_dialog_is_open`` short-circuited on ``count() == 0`` before spending its timeout budget. After ``goto(/preload/custom-invite/?vanityName=)`` resolves on ``domcontentloaded``, the modal mounts a few hundred ms later, so the count check fired against an empty DOM and returned False immediately. ``wait_for(state="visible")`` is the right primitive here — it polls until the element exists *and* is visible. The minimal fix: * Broaden ``_DIALOG_SELECTOR`` to ``dialog[open], [role="dialog"], [role="alertdialog"], [aria-modal="true"]`` — covers every modal variant LinkedIn ships today across the artdeco-modal versions, with the same scope applied to ``_DIALOG_TEXTAREA_SELECTOR`` so the note textarea is still found under the new dialog roots. * Replace the eager ``count()`` short-circuit in ``_dialog_is_open`` with ``wait_for(state="visible", timeout=timeout)`` directly. The helper's contract — "is a dialog currently open" — already implies waiting up to ``timeout`` for one, and every caller already passes the timeout it actually wants. The 2-button gating fix from a48fccc is preserved: with the dialog correctly detected, the existing ``btn_count >= 2`` / ``nth(btn_count - 2)`` branch fires and reveals the textarea on the new gating layout, while the no-note path still clicks "Send without a note" via ``_click_dialog_primary_button``. Tests: * ``test_dialog_selector_matches_alertdialog_and_aria_modal`` — guards every required ARIA pattern in the selector constants. * ``test_dialog_is_open_waits_for_late_mounting_dialog`` — asserts the helper spends its budget on ``wait_for(visible)`` rather than reading ``count()`` eagerly. * Existing ``test_submit_invite_dialog_handles_two_button_gating_dialog`` and the rest of ``TestConnectWithPerson`` continue to pass unchanged. * Full suite: 510 passed (was 508). Re stickerdaniel#455.
Greptile SummaryThis PR updates the LinkedIn connect invite dialog handling. It changes:
Confidence Score: 3/5This should be fixed before merging.
linkedin_mcp_server/scraping/extractor.py Important Files Changed
|
The comma-separated _DIALOG_SELECTOR (dialog[open], [role="dialog"], [role="alertdialog"], [aria-modal="true"]) breaks descendant selectors: appending " button" only attaches to the LAST arm, so the other 3 arms match dialog roots instead of buttons. For invite modals rooted at role="alertdialog" (the live LinkedIn layout), this means _click_dialog_primary_button can click the modal root instead of "Send", and the gating-button branch in _submit_invite_dialog can click the root instead of "Add a note" — both leaving the dialog open and returning connect_unavailable. Wrap _DIALOG_SELECTOR in :is() at all 3 descendant-button locator sites to apply the descendant combinator across all selector arms. Reproduces the failure observed in production after the previous selector-broadening commit: invites against alertdialog-rooted modals returned connect_unavailable across multiple profiles. Fix matches Greptile's review suggestion on PR stickerdaniel#458.
The comma-separated _DIALOG_SELECTOR (dialog[open], [role="dialog"], [role="alertdialog"], [aria-modal="true"]) breaks descendant selectors: appending " button" only attaches to the LAST arm, so the other 3 arms match dialog roots instead of buttons. For invite modals rooted at role="alertdialog" (the live LinkedIn layout), this means _click_dialog_primary_button can click the modal root instead of "Send", and the gating-button branch in _submit_invite_dialog can click the root instead of "Add a note" — both leaving the dialog open and returning connect_unavailable. Wrap _DIALOG_SELECTOR in :is() at all 3 descendant-button locator sites to apply the descendant combinator across all selector arms. Reproduces the failure observed in production after the previous selector-broadening commit: invites against alertdialog-rooted modals returned connect_unavailable across multiple profiles. Fix matches Greptile's review suggestion on PR stickerdaniel#458.
|
Greptile's review caught a real bug — confirmed in production. After the dialog-selector broadening on this PR, my own invite attempts against Pushed 50a695b wrapping Will verify against my account and report back. |
…rrow selector Yesterday's production trace (run-3mu94g3t step 61) shows successful invite submission to gregsandzimier with the original narrow selector 'dialog[open], [role="dialog"]'. This proves LinkedIn's invite dialog uses role="dialog" — not role="alertdialog" as previously assumed. Today's failure traces (run-e0hlk958) show the dialog renders identically. With the broadened selector, .first picks a hidden "aria-modal" element elsewhere on the page (e.g. messaging widget), and wait_for(visible) times out → connect_unavailable. Reverting _DIALOG_SELECTOR + _DIALOG_TEXTAREA_SELECTOR to narrow form. Keeps :is() scoping fix and btn_count >= 2 gating-dialog fix from PR stickerdaniel#456 — both still load-bearing.
…-fallback REAL ROOT CAUSE for issue stickerdaniel#455 / stickerdaniel#458 'connect_unavailable' failures in production: LinkedIn limits free accounts to a small number of personalized notes per month. After the quota is exhausted, clicking 'Add a note' in the 2-button gating dialog replaces the dialog with a Premium upsell: 'Send unlimited personalized invites with Premium — You're out of free custom notes.' Previous code waited 3s for the textarea to appear, then failed via _fill_dialog_textarea returning False, then dismissed and returned 'connect_unavailable' — discarding the connection request entirely even though the no-note send path was still viable. Fix: detect the upsell by its content, dismiss it, re-trigger the invite dialog (URL is unchanged through the upsell), and fall through to the primary-button click which sends without a note. note_sent=False is returned so the caller knows the personalization was dropped. Verified end-to-end against live LinkedIn: confirmed Premium upsell appears for an out-of-quota account, dismiss + re-navigate restores the original gating dialog, primary-button click sends invite. Profile transitions to 'Pending'. (Tested on babermehkeri and javargas209.)
…-fallback REAL ROOT CAUSE for issue stickerdaniel#455 / stickerdaniel#458 'connect_unavailable' failures in production: LinkedIn limits free accounts to a small number of personalized notes per month. After the quota is exhausted, clicking 'Add a note' in the 2-button gating dialog replaces the dialog with a Premium upsell: 'Send unlimited personalized invites with Premium — You're out of free custom notes.' Previous code waited 3s for the textarea to appear, then failed via _fill_dialog_textarea returning False, then dismissed and returned 'connect_unavailable' — discarding the connection request entirely even though the no-note send path was still viable. Fix: detect the upsell by its content, dismiss it, re-trigger the invite dialog (URL is unchanged through the upsell), and fall through to the primary-button click which sends without a note. note_sent=False is returned so the caller knows the personalization was dropped. Verified end-to-end against live LinkedIn: confirmed Premium upsell appears for an out-of-quota account, dismiss + re-navigate restores the original gating dialog, primary-button click sends invite. Profile transitions to 'Pending'. (Tested on babermehkeri and javargas209.)
|
Found the actual root cause via live-DOM probe. What was happening: When the free-note monthly quota is exhausted, LinkedIn replaces the 'Add a note?' gating dialog with a 'Send unlimited personalized invites with Premium — You're out of free custom notes' upsell modal. The code waited for the textarea (which never mounts), then dismissed and returned The Greptile-flagged Probe-validated end-to-end against live LinkedIn:
Caller still sees Verified against |
Follow-up to #456 (merged). The 2-button-gating handling shipped in #456 is correct for the case where the dialog actually opens, but on real accounts post-deploy
connect_with_personwas still returningconnect_unavailablefor every with-note attempt — because_dialog_is_openwas failing one layer upstream and the secondary-button branch never ran.This PR fixes the upstream gate. The 2-button-gating handling from #456 is preserved verbatim and is now reachable.
Re #455 (post-#456 follow-up). Distinct from #454 (different failure stage — that one bails before reaching the deeplink gate) and #432 (composer-overlay collision — orthogonal).
Real root cause
Two issues in
_dialog_is_open/_DIALOG_SELECTOR, both visible in tracerun-8fic76qwfrom a failing 8/8 production run on 2026-05-20 (post-#456 deploy):Selector was too narrow. The "Add a note to your invitation?" gating dialog is an
artdeco-modalwhose root carriesrole="alertdialog"and/oraria-modal="true", neither of which matcheddialog[open], [role="dialog"]. Trace screenshots show the dialog visibly mounted at the exact DOM moment_dialog_is_openwas returning False — the helper just couldn't see it.Race against modal mount.
_dialog_is_openshort-circuited oncount() == 0before spending its timeout budget. Aftergoto(/preload/custom-invite/?vanityName=)resolves ondomcontentloaded, the modal mounts a few hundred ms later, so the count check fired against an empty DOM and returned False immediately.wait_for(state="visible")is the right primitive — it polls until the element exists and is visible.The two effects compound. Even on accounts where LinkedIn picks
role="dialog"and the selector would have matched, the eagercount()made the helper racy. Even on a slow account where waiting longer would have helped, the wrong selector would still miss the alertdialog mount.Evidence
From the trace artifact
~/.linkedin-mcp/trace-runs/run-8fic76qw/(real failing run, 2026-05-20, account-wide reproduction matching #455's reporter):goto preload/custom-invite/?vanityName=meenal-ganesh-b1710321. Screenshot005-extractor-after-goto.pngshows the gating dialog fully mounted and visible (X close + "Add a note" + "Send without a note"). Body marker:"Dialog content start. Add a note to your invitation? Personalize your invitation to Meenal Ganesh by adding a note."— verbatim DOM text.submitted=True, dialog detected, "Send without a note" clicked, profile transitioned to"More Pending Message". Invite landed.submitted=False→connect_unavailable. Same dialog DOM, different_dialog_is_openresult. Only the LinkedIn-side rollout state differed.The fix
Per-call sites: every existing reference to
_DIALOG_SELECTORand_DIALOG_TEXTAREA_SELECTOR(button collection in_submit_invite_dialog, textarea fill in_fill_dialog_textarea, dismiss-on-hidden) gets the broadened scope automatically.The 2-button-gating handling from #456 is preserved — with the dialog now correctly detected, the existing
btn_count >= 2/nth(btn_count - 2)branch fires and reveals the textarea on the new gating layout, while the no-note path still hits "Send without a note" via_click_dialog_primary_button.Tests
test_dialog_selector_matches_alertdialog_and_aria_modal— guards every required ARIA pattern in_DIALOG_SELECTORand_DIALOG_TEXTAREA_SELECTOR. Future narrowing regressions fail this test before merging.test_dialog_is_open_waits_for_late_mounting_dialog— asserts the helper spends its budget onwait_for(visible)rather than readingcount()eagerly.test_submit_invite_dialog_handles_two_button_gating_dialog(from fix(connect): match alertdialog/aria-modal and wait for late-mounting invite modal #456) and the rest ofTestConnectWithPersoncontinue to pass unchanged.Verification confidence
This is best-effort code reading + DOM trace evidence — I did not drive a real browser to verify the new selector against the live LinkedIn DOM. What I do know:
"Dialog content start. ...") is fetched viadocument.body.innerText, so the dialog is in the DOM._dialog_is_openwas returning False against that DOM; only an out-of-scope selector or a race against mount can produce that.wait_for(visible)pattern is what the rest of the codebase already uses for modal detection (cf.extract_overlay_pageandhandle_modal_close).If LinkedIn ever serves a modal with neither
roleattribute noraria-modal, this fix won't catch it — but I've not seen evidence of that variant in any trace, and adding.artdeco-modal__contentwould risk matching non-modal artdeco UI elsewhere on the page.What this PR does not do
btn_count >= 2change — it's load-bearing once the dialog is detected._open_more_menuandclick_button_by_texthelpers for that."did not expose a usable Connect action"on profiles with visible Connect). Those are separate failure stages with different signatures.🤖 Generated with Claude Code