Fix/344 bypass patchright compose box actionability in send_message#362
Fix/344 bypass patchright compose box actionability in send_message#362dannersm wants to merge 5 commits intostickerdaniel:mainfrom
Conversation
patchright's wait_for(state="visible") times out on LinkedIn's React-hydrated contenteditable compose box even when count() > 0 and the element is fully visible (display:block, non-zero bbox, no inert ancestor). Same quirk affects click(), press_sequentially(), and locator-based interactions. Closes stickerdaniel#344. Four targeted fixes in _resolve_message_compose_box and send_message: 1. Skip wait_for when count() > 0 — return locator.last directly. 2. Build full compose URL (?profileUrn=...&screenContext=...&interop= msgOverlay) to avoid the "Say hello" widget shown for new connections when using the minimal ?recipient=<URN> form. 3. Focus compose box via page.evaluate() + type via page.keyboard.type() instead of click()/press_sequentially() — bypasses actionability checks while still firing real key events React needs to enable Send. 4. Click send button via JS (no actionability) + Enter fallback instead of locator.click(). DOM selectors use only type, aria-label, and data attributes — no layout class names. Verified in production: recipient_selected: true, sent: true across multiple leads including new connections (<24h old). Generated with Claude Sonnet 4.6
Cover the four targeted fixes from 280fcdc: 1. _resolve_message_compose_box returns locator.last when count() > 0, skipping the wait_for that stalls under patchright. Also tests the fallback path (count raises, all selectors miss). 2. Compose URL includes profileUrn, screenContext, and interop params for new-connection compatibility. 3. send_message focuses via page.evaluate() and types via page.keyboard.type() — asserts compose_interact_failed on focus failure. 4. Enter key fallback when JS cannot find a visible send button. Also adds a clarifying comment on the wait_for fallback block in _resolve_message_compose_box explaining it covers the count()-exception path for non-patchright drivers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR fixes Confidence Score: 5/5Safe to merge — all P1 concerns from the previous review round are resolved and the only remaining finding is a P2 comment clarification. Both previously flagged issues (dead mock assertions and missing asyncio.sleep patch) are fixed. The implementation logic is correct, selectors are ARIA-only and stable, and tests cover the early-return, fallback, and typing paths. The sole remaining finding is a misleading inline comment, not a correctness issue. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A([send_message called]) --> B{profile_urn provided?}
B -- Yes --> C[Build full compose URL\nprofileUrn + recipient +\nscreenContext + interop]
B -- No --> D[_resolve_message_compose_href]
D -->|None| E([message_unavailable])
C --> F[Navigate to compose URL]
D -->|URL| F
F --> G[_wait_for_message_surface]
G -->|recipient_picker| H[_select_message_recipient]
H -->|failed| I([recipient_resolution_failed])
H -->|ok| G
G -->|composer| J[_resolve_message_compose_box]
J --> K{count > 0?}
K -- Yes --> L[return locator.last\nno wait_for]
K -- No/Exception --> M[wait_for fallback\nnon-patchright path]
M -->|timeout| N([None → composer_unavailable])
L --> O[_compose_page_matches_recipient]
O -->|no match| P([recipient_resolution_failed])
O -->|match| Q{confirm_send?}
Q -- No --> R([confirmation_required])
Q -- Yes --> S[JS evaluate → el.focus]
S -->|false| T([compose_interact_failed])
S -->|true| U[keyboard.type message]
U --> V[JS evaluate → btn.click]
V -->|false| W[keyboard.press Enter]
V -->|true| X[_message_text_visible]
W --> X
X -->|false| Y([send_unavailable])
X -->|true| Z([sent])
Prompt To Fix All With AIThis is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 1501-1504
Comment:
**Fallback comment understates when it applies**
The comment says the `wait_for` fallback runs "when `count()` raised an exception above (`candidate_count` is `None`)". But the code also falls through to `wait_for` when `count()` returns `0` (element not found), because `if candidate_count and candidate_count > 0` is falsy for `0`. That second case — where the element is simply absent at the time of the `count()` call — is the more common trigger for `wait_for`, and is arguably the one where a non-patchright driver might legitimately succeed if the element appears slightly later.
```suggestion
# Fallback: when count() raised an exception (candidate_count is None)
# or returned 0, attempt the original wait_for path. This is unlikely
# to succeed under patchright (same actionability quirk), but preserves
# the prior behaviour for non-patchright drivers where wait_for works
# normally and the element may still appear within the timeout.
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (3): Last reviewed commit: "fix: Address PR review feedback" | Re-trigger Greptile |
|
@stickerdaniel would appreciate a review since the send message tool is not currently working. thx! |
|
I just tested the send_message tool in this PR. For me it only worked if I pass |
Hey @hugobiais any chance you could point me to which function in the extractor is giving you timeout? Is it the |
Actually I just tested again and didn't get the timeout issue! |
|
Thanks for the PR. I would drop the [aria-label*="Escribe un mensaje"] selector, why only Spanish? LinkedIn localizes this label into xx languages... |
- Drop Spanish-only aria-label from compose focus selector; generic contenteditable fallback already covers all locales - Patch asyncio.sleep in TestSendMessageComposerInteraction helper so tests no longer burn 1.4s of real wall time per run - Set wait_for mocks before the call in test_returns_locator_when_count_positive and assert_not_called() after, so the early-return invariant is actually verified
Problem
send_messagewas broken under patchright:locator.wait_for(state="visible")times out on the contenteditable compose div even though the element is
fully visible by every CSS/DOM criterion (display:block, visibility:visible,
non-zero bounding box, no inert ancestor). Same actionability check also
blocked
compose_box.click()andpress_sequentially().Root cause appears to be a patchright bug with React-hydrated contenteditable
elements in isolated worlds.
Changes
Compose box resolution (
_resolve_message_compose_box):Skip
wait_forwhencount() > 0already confirms the element is present.Preserve the original
wait_forpath as a fallback for non-patchright drivers.Compose URL (
send_messagewithprofile_urn):Build the full URL LinkedIn's own Message button generates:
?profileUrn=...&recipient=...&screenContext=NON_SELF_PROFILE_VIEW&interop=msgOverlayThe minimal
?recipient=<URN>form showed a "Say hello" widget (no composebox) for profiles not yet connected. The full URL consistently opens the
real composer regardless of connection age.
Typing and send (
send_message):Replace
compose_box.click()+press_sequentially()+send_button.click()(all blocked by actionability) with:
page.evaluate()→el.focus()(bypasses actionability)page.keyboard.type()(operates on active element, fires real React events)page.evaluate()→btn.click()on first visible/enabled send buttonpage.keyboard.press("Enter")as fallback if no send button foundAll selectors use only ARIA attributes (
role,contenteditable,aria-label,data-control-name).Tests
test_profile_urn_compose_url_includes_full_params— verifies full URL paramsTestResolveMessageComposeBox— early return, all-miss, count() exception pathsTestSendMessageComposerInteraction— focus+type path, focus failure, Enter fallback