Skip to content

Fix/344 bypass patchright compose box actionability in send_message#362

Open
dannersm wants to merge 5 commits intostickerdaniel:mainfrom
dannersm:fix/344-patchright-compose-box-actionability
Open

Fix/344 bypass patchright compose box actionability in send_message#362
dannersm wants to merge 5 commits intostickerdaniel:mainfrom
dannersm:fix/344-patchright-compose-box-actionability

Conversation

@dannersm
Copy link
Copy Markdown

@dannersm dannersm commented Apr 13, 2026

Problem

send_message was 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() and press_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_for when count() > 0 already confirms the element is present.
Preserve the original wait_for path as a fallback for non-patchright drivers.

Compose URL (send_message with profile_urn):
Build the full URL LinkedIn's own Message button generates:
?profileUrn=...&recipient=...&screenContext=NON_SELF_PROFILE_VIEW&interop=msgOverlay
The minimal ?recipient=<URN> form showed a "Say hello" widget (no compose
box) 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:

  1. page.evaluate()el.focus() (bypasses actionability)
  2. page.keyboard.type() (operates on active element, fires real React events)
  3. page.evaluate()btn.click() on first visible/enabled send button
  4. page.keyboard.press("Enter") as fallback if no send button found

All selectors use only ARIA attributes (role, contenteditable, aria-label,
data-control-name).

Tests

  • test_profile_urn_compose_url_includes_full_params — verifies full URL params
  • TestResolveMessageComposeBox — early return, all-miss, count() exception paths
  • TestSendMessageComposerInteraction — focus+type path, focus failure, Enter fallback

dannersm and others added 2 commits April 11, 2026 22:50
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>
@dannersm dannersm changed the title Fix/344 bypass patchright compose box actionability in send Fix/344 bypass patchright compose box actionability in send_message Apr 13, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 13, 2026

Greptile Summary

This PR fixes send_message under patchright by replacing the broken wait_for(state="visible") actionability path with a count()-based presence check, updating the compose URL built from profile_urn to include all parameters LinkedIn's own Message button generates, and replacing click()/press_sequentially() with page.evaluate() focus + page.keyboard.type(). Both previously flagged concerns — the dead mock assignments in test_returns_locator_when_count_positive and the missing asyncio.sleep patch in _patch_send_message_to_compose — are resolved in this revision.

Confidence Score: 5/5

Safe 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

Filename Overview
linkedin_mcp_server/scraping/extractor.py Adds patchright workarounds: count()-based compose-box resolution, full-param compose URL for profile_urn, and JS focus + keyboard.type for typing and sending. Logic is sound; one minor comment inaccuracy in the fallback path.
tests/test_scraping.py Adds TestResolveMessageComposeBox and TestSendMessageComposerInteraction with correct mock setup, asyncio.sleep patched in the shared helper, and wait_for assertions properly placed after the call.

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])
Loading
Prompt To Fix All With AI
This 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

@hugobiais
Copy link
Copy Markdown

@stickerdaniel would appreciate a review since the send message tool is not currently working. thx!

@hugobiais
Copy link
Copy Markdown

@dannersm @stickerdaniel

I just tested the send_message tool in this PR. For me it only worked if I pass --timeout 15000 to it.

@dannersm
Copy link
Copy Markdown
Author

dannersm commented Apr 14, 2026

@dannersm @stickerdaniel

I just tested the send_message tool in this PR. For me it only worked if I pass --timeout 15000 to it.

Hey @hugobiais any chance you could point me to which function in the extractor is giving you timeout? Is it the resolve_message_compose_box? Hate to be that guy but it works on my machine with the defaults XD

@hugobiais
Copy link
Copy Markdown

@dannersm @stickerdaniel
I just tested the send_message tool in this PR. For me it only worked if I pass --timeout 15000 to it.

Hey @hugobiais any chance you could point me to which function in the extractor is giving you timeout? Is it the resolve_message_compose_box? Hate to be that guy but it works on my machine with the defaults XD

Actually I just tested again and didn't get the timeout issue!

@stickerdaniel
Copy link
Copy Markdown
Owner

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants