@@ -97,14 +97,18 @@ def _connection_result(
9797 message : str ,
9898 * ,
9999 note_sent : bool = False ,
100+ profile : str = "" ,
100101) -> dict [str , Any ]:
101102 """Build a structured response for a profile connection attempt."""
102- return {
103+ result : dict [ str , Any ] = {
103104 "url" : url ,
104105 "status" : status ,
105106 "message" : message ,
106107 "note_sent" : note_sent ,
107108 }
109+ if profile :
110+ result ["profile" ] = profile
111+ return result
108112
109113
110114def _normalize_csv (value : str , mapping : dict [str , str ]) -> str :
@@ -393,25 +397,28 @@ async def get_page_text(self) -> str:
393397 async def click_button_by_text (
394398 self , text : str , * , scope : str = "main" , timeout : int = 5000
395399 ) -> bool :
396- """Click the first button/link matching * text* within *scope *.
400+ """Click the first button/link whose visible text is exactly *text *.
397401
398- The text comes from LLM analysis at runtime — not hardcoded.
402+ Uses a regex filter for exact matching to avoid substring false
403+ positives (e.g. "Connect" matching "connections").
399404 Returns True if clicked, False if no match found.
400405 """
401- selector = (
402- f' { scope } button:has-text(" { text } "), '
403- f' { scope } a:has-text(" { text } "), '
404- f' { scope } [role="button"]:has-text(" { text } ")'
406+ matches = (
407+ self . _page . locator ( scope )
408+ . locator ( "button, a, [role='button']" )
409+ . filter ( has_text = re . compile ( rf"^ { re . escape ( text ) } $" ))
405410 )
406- locator = self ._page .locator (selector ).first
411+ count = await matches .count ()
412+ logger .debug ("click_button_by_text(%r): %d matches in %s" , text , count , scope )
413+ if count == 0 :
414+ return False
415+ target = matches .first
407416 try :
408- if await self ._page .locator (selector ).count () == 0 :
409- return False
410- await locator .scroll_into_view_if_needed (timeout = timeout )
417+ await target .scroll_into_view_if_needed (timeout = timeout )
411418 except Exception :
412419 logger .debug ("Scroll failed for button '%s'" , text , exc_info = True )
413420 try :
414- await locator .click (timeout = timeout )
421+ await target .click (timeout = timeout )
415422 return True
416423 except Exception :
417424 logger .debug ("Click failed for button '%s'" , text , exc_info = True )
@@ -777,25 +784,31 @@ async def connect_with_person(
777784
778785 if state == "already_connected" :
779786 return _connection_result (
780- url , "already_connected" , "You are already connected with this profile."
787+ url ,
788+ "already_connected" ,
789+ "You are already connected with this profile." ,
790+ profile = page_text ,
781791 )
782792 if state == "pending" :
783793 return _connection_result (
784794 url ,
785795 "pending" ,
786796 "A connection request is already pending for this profile." ,
797+ profile = page_text ,
787798 )
788799 if state == "follow_only" :
789800 return _connection_result (
790801 url ,
791802 "follow_only" ,
792803 "This profile currently exposes Follow but not Connect." ,
804+ profile = page_text ,
793805 )
794806 if state == "unavailable" :
795807 return _connection_result (
796808 url ,
797809 "connect_unavailable" ,
798810 "LinkedIn did not expose a usable Connect action for this profile." ,
811+ profile = page_text ,
799812 )
800813
801814 # state is "connectable" or "incoming_request"
@@ -864,6 +877,9 @@ async def connect_with_person(
864877 except PlaywrightTimeoutError :
865878 logger .debug ("Dialog did not close after clicking send" )
866879
880+ # Read the current page text (already on the profile after the action)
881+ updated_text = await self .get_page_text ()
882+
867883 status = "accepted" if state == "incoming_request" else "connected"
868884 return _connection_result (
869885 url ,
@@ -872,6 +888,7 @@ async def connect_with_person(
872888 if status == "connected"
873889 else "Connection request accepted." ,
874890 note_sent = note_sent ,
891+ profile = updated_text ,
875892 )
876893
877894 async def scrape_company (
0 commit comments