Skip to content

Commit 864473d

Browse files
Merge pull request #289 from stickerdaniel/03-29-feat_connect_add_more_menu_support_and_return_profile_text
feat(connect): Add More menu support and return profile text
2 parents b682dd7 + 974014f commit 864473d

2 files changed

Lines changed: 49 additions & 9 deletions

File tree

linkedin_mcp_server/scraping/extractor.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,38 @@ async def _dismiss_dialog(self) -> None:
470470
except PlaywrightTimeoutError:
471471
pass
472472

473+
async def _open_more_menu(self) -> bool:
474+
"""Open the profile's More (three-dot) menu and check for Connect.
475+
476+
Uses ``aria-label`` to find the More button (language-independent)
477+
and ``[role="menu"]`` to detect the opened menu (structural).
478+
Returns True if the menu opened and contains a Connect option.
479+
"""
480+
more_btn = self._page.locator("main button[aria-label*='More']")
481+
try:
482+
if await more_btn.count() == 0:
483+
return False
484+
await more_btn.first.click(timeout=5000)
485+
except Exception:
486+
logger.debug("Could not click More button", exc_info=True)
487+
return False
488+
489+
try:
490+
await self._page.wait_for_selector("[role='menu']", timeout=3000)
491+
except PlaywrightTimeoutError:
492+
logger.debug("More menu did not appear")
493+
return False
494+
495+
# Check if Connect is in the menu
496+
menu_connect = (
497+
self._page.locator("[role='menu']")
498+
.locator("button, a, li, [role='menuitem'], [role='button']")
499+
.filter(has_text=re.compile(r"^Connect$"))
500+
)
501+
count = await menu_connect.count()
502+
logger.debug("More menu Connect matches: %d", count)
503+
return count > 0
504+
473505
async def extract_page(
474506
self,
475507
url: str,
@@ -796,13 +828,20 @@ async def connect_with_person(
796828
"A connection request is already pending for this profile.",
797829
profile=page_text,
798830
)
831+
via_more_menu = False
799832
if state == "follow_only":
800-
return _connection_result(
801-
url,
802-
"follow_only",
803-
"This profile currently exposes Follow but not Connect.",
804-
profile=page_text,
805-
)
833+
# Connect may be hidden behind the More (three-dot) menu
834+
if await self._open_more_menu():
835+
state = "connectable"
836+
via_more_menu = True
837+
else:
838+
return _connection_result(
839+
url,
840+
"follow_only",
841+
"This profile currently exposes Follow but not Connect.",
842+
profile=page_text,
843+
)
844+
806845
if state == "unavailable":
807846
return _connection_result(
808847
url,
@@ -821,7 +860,8 @@ async def connect_with_person(
821860
)
822861

823862
# Click the button (page is already loaded from scrape_person)
824-
clicked = await self.click_button_by_text(button_text)
863+
click_scope = "[role='menu']" if via_more_menu else "main"
864+
clicked = await self.click_button_by_text(button_text, scope=click_scope)
825865
if not clicked:
826866
return _connection_result(
827867
url,

tests/test_scraping.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ async def test_connectable_clicks_connect(self, mock_page):
810810

811811
assert result["status"] == "connected"
812812
assert result["url"] == "https://www.linkedin.com/in/testuser/"
813-
mock_click.assert_awaited_once_with("Connect")
813+
mock_click.assert_awaited_once_with("Connect", scope="main")
814814

815815
async def test_returns_already_connected(self, mock_page):
816816
extractor = LinkedInExtractor(mock_page)
@@ -852,7 +852,7 @@ async def test_returns_incoming_request_accepted(self, mock_page):
852852
result = await extractor.connect_with_person("testuser")
853853

854854
assert result["status"] == "accepted"
855-
mock_click.assert_awaited_once_with("Accept")
855+
mock_click.assert_awaited_once_with("Accept", scope="main")
856856

857857
async def test_returns_follow_only(self, mock_page):
858858
extractor = LinkedInExtractor(mock_page)

0 commit comments

Comments
 (0)