Skip to content

Commit 1d057ef

Browse files
committed
fix: add aria-label fallback for more robust UI toggle clicks
1 parent 300cb49 commit 1d057ef

5 files changed

Lines changed: 102 additions & 61 deletions

File tree

browser_utils/debug_utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
- Human-readable Texas timestamps
88
- Complete metadata capture
99
10-
Created: 2025-11-21
1110
Purpose: Fix headless mode debugging and client disconnect issues
1211
"""
1312

browser_utils/page_controller_modules/thinking.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -467,12 +467,20 @@ async def _control_thinking_mode_toggle(
467467
raise
468468
except Exception:
469469
try:
470-
root = self.page.locator(
471-
'mat-slide-toggle[data-test-toggle="enable-thinking"]'
470+
# 新版UI: 尝试直接点击带 aria-label 的开关父容器
471+
alt_toggle = self.page.locator(
472+
'mat-slide-toggle:has(button[aria-label="Toggle thinking mode"])'
472473
)
473-
label = root.locator("label.mdc-label")
474-
await expect_async(label).to_be_visible(timeout=2000)
475-
await label.click(timeout=CLICK_TIMEOUT_MS)
474+
if await alt_toggle.count() > 0:
475+
await alt_toggle.click(timeout=CLICK_TIMEOUT_MS)
476+
else:
477+
# 旧版UI回退: data-test-toggle
478+
root = self.page.locator(
479+
'mat-slide-toggle[data-test-toggle="enable-thinking"]'
480+
)
481+
label = root.locator("label.mdc-label")
482+
await expect_async(label).to_be_visible(timeout=2000)
483+
await label.click(timeout=CLICK_TIMEOUT_MS)
476484
except Exception:
477485
raise
478486
await self._check_disconnect(
@@ -553,12 +561,20 @@ async def _control_thinking_budget_toggle(
553561
raise
554562
except Exception:
555563
try:
556-
root = self.page.locator(
557-
'mat-slide-toggle[data-test-toggle="manual-budget"]'
564+
# 新版UI: 尝试直接点击带 aria-label 的开关父容器
565+
alt_toggle = self.page.locator(
566+
'mat-slide-toggle:has(button[aria-label="Toggle thinking budget between auto and manual"])'
558567
)
559-
label = root.locator("label.mdc-label")
560-
await expect_async(label).to_be_visible(timeout=2000)
561-
await label.click(timeout=CLICK_TIMEOUT_MS)
568+
if await alt_toggle.count() > 0:
569+
await alt_toggle.click(timeout=CLICK_TIMEOUT_MS)
570+
else:
571+
# 旧版UI回退: data-test-toggle
572+
root = self.page.locator(
573+
'mat-slide-toggle[data-test-toggle="manual-budget"]'
574+
)
575+
label = root.locator("label.mdc-label")
576+
await expect_async(label).to_be_visible(timeout=2000)
577+
await label.click(timeout=CLICK_TIMEOUT_MS)
562578
except Exception:
563579
raise
564580
await self._check_disconnect(

config/selector_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# Google AI Studio 会不定期更改 UI 结构,此列表包含所有已知的容器选择器
1818
# 优先尝试当前 UI,回退到旧 UI
1919
INPUT_WRAPPER_SELECTORS: List[str] = [
20-
# 当前 UI 结构 (ms-prompt-input-wrapper / ms-chunk-editor) - 2024年12月后
20+
# 当前 UI 结构 (ms-prompt-input-wrapper / ms-chunk-editor)
2121
"ms-prompt-input-wrapper .prompt-input-wrapper",
2222
"ms-prompt-input-wrapper",
2323
"ms-chunk-editor",

config/selectors.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# 主输入 textarea 兼容当前和旧 UI 结构
88
# 当前结构: ms-prompt-input-wrapper > ... > ms-autosize-textarea > textarea.textarea
99
PROMPT_TEXTAREA_SELECTOR = (
10-
# 当前 UI 结构 (2024年12月后)
10+
# 当前 UI 结构
1111
"textarea.textarea, " # 最直接的选择器
1212
"ms-autosize-textarea textarea, "
1313
"ms-chunk-input textarea, "
@@ -81,17 +81,33 @@
8181

8282
# --- 思考模式相关选择器 ---
8383
# 主思考开关:控制是否启用思考模式(总开关)
84+
# Flash模型使用 aria-label="Toggle thinking mode"
85+
# 回退: 旧版 data-test-toggle 属性
8486
ENABLE_THINKING_MODE_TOGGLE_SELECTOR = (
87+
'button[role="switch"][aria-label="Toggle thinking mode"], '
8588
'mat-slide-toggle[data-test-toggle="enable-thinking"] button[role="switch"].mdc-switch, '
8689
'[data-test-toggle="enable-thinking"] button[role="switch"].mdc-switch'
8790
)
8891
# 手动预算开关:控制是否手动限制思考预算
92+
# Flash模型使用 aria-label="Toggle thinking budget between auto and manual"
93+
# 回退: 旧版 data-test-toggle 属性
8994
SET_THINKING_BUDGET_TOGGLE_SELECTOR = (
95+
'button[role="switch"][aria-label="Toggle thinking budget between auto and manual"], '
9096
'mat-slide-toggle[data-test-toggle="manual-budget"] button[role="switch"].mdc-switch, '
9197
'[data-test-toggle="manual-budget"] button[role="switch"].mdc-switch'
9298
)
9399
# 思考预算输入框
94-
THINKING_BUDGET_INPUT_SELECTOR = '[data-test-slider] input[type="number"]'
100+
# 思考预算滑块具有独特的 min="512" 属性(温度是 max="2",TopP 是 max="1")
101+
# 优先使用最精确的选择器,保留多层回退以应对 UI 变化
102+
THINKING_BUDGET_INPUT_SELECTOR = (
103+
# 最精确: 使用 data-test-id 容器 + spinbutton
104+
'[data-test-id="user-setting-budget-animation-wrapper"] input[type="number"], '
105+
# 回退1: 使用独特的 min="512" 属性定位(仅思考预算滑块有此属性)
106+
'input.slider-number-input[min="512"], '
107+
'ms-slider input[type="number"][min="512"], '
108+
# 回退2: 旧版 data-test-slider 属性
109+
'[data-test-slider] input[type="number"]'
110+
)
95111

96112
# 思考等级下拉
97113
THINKING_LEVEL_SELECT_SELECTOR = '[role="combobox"][aria-label="Thinking Level"], mat-select[aria-label="Thinking Level"], [role="combobox"][aria-label="Thinking level"], mat-select[aria-label="Thinking level"]'

tests/browser_utils/page_controller_modules/test_thinking.py

Lines changed: 57 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -314,20 +314,23 @@ async def test_set_thinking_budget_value_max_fallback(mock_controller, mock_page
314314
async def test_control_thinking_mode_toggle_click_failure_fallback(
315315
mock_controller, mock_page
316316
):
317-
# Test fallback to label click if toggle click fails
317+
# Test fallback to aria-label based toggle click if main toggle click fails
318318
toggle = AsyncMock()
319319
toggle.click.side_effect = Exception("Not clickable")
320320

321-
label = AsyncMock()
321+
alt_toggle = AsyncMock()
322+
alt_toggle.count = AsyncMock(return_value=1)
322323

323324
def locator_side_effect(selector):
324325
if "button" in selector and "switch" in selector:
325326
return toggle
326-
if "mat-slide-toggle" in selector: # Root for fallback
327+
if 'aria-label="Toggle thinking mode"' in selector:
328+
return alt_toggle
329+
if "mat-slide-toggle" in selector: # Old fallback
327330
root = MagicMock()
328-
root.locator.return_value = label
331+
root.locator.return_value = AsyncMock()
329332
return root
330-
return toggle # Default for first locator call
333+
return toggle
331334

332335
mock_page.locator.side_effect = locator_side_effect
333336
toggle.get_attribute.return_value = "false"
@@ -344,7 +347,7 @@ def locator_side_effect(selector):
344347
True, MagicMock(return_value=False)
345348
)
346349

347-
label.click.assert_called()
350+
alt_toggle.click.assert_called()
348351

349352

350353
@pytest.mark.asyncio
@@ -623,20 +626,23 @@ async def test_set_thinking_level_errors(mock_controller):
623626

624627
@pytest.mark.asyncio
625628
async def test_control_thinking_mode_toggle_fallback(mock_controller):
626-
# Test fallback to label click
629+
# Test fallback to aria-label based toggle click
627630
toggle = MagicMock()
628631
toggle.get_attribute = AsyncMock(return_value="false")
629632
toggle.click = AsyncMock(side_effect=Exception("Click failed"))
630633

631-
label = MagicMock()
632-
label.click = AsyncMock()
633-
root = MagicMock()
634-
root.locator.return_value = label
634+
alt_toggle = MagicMock()
635+
alt_toggle.count = AsyncMock(return_value=1)
636+
alt_toggle.click = AsyncMock()
635637

636638
def locator_side_effect(selector):
637-
if "button" in selector and 'data-test-toggle="enable-thinking"' in selector:
639+
if "button" in selector and "switch" in selector:
638640
return toggle
641+
if 'aria-label="Toggle thinking mode"' in selector:
642+
return alt_toggle
639643
if 'data-test-toggle="enable-thinking"' in selector:
644+
root = MagicMock()
645+
root.locator.return_value = MagicMock()
640646
return root
641647
return toggle
642648

@@ -653,25 +659,28 @@ def locator_side_effect(selector):
653659
):
654660
await mock_controller._control_thinking_mode_toggle(True, check_disconnect_mock)
655661

656-
label.click.assert_called()
662+
alt_toggle.click.assert_called()
657663

658664

659665
@pytest.mark.asyncio
660666
async def test_control_thinking_budget_toggle_fallback(mock_controller):
661-
# Test fallback to label click
667+
# Test fallback to aria-label based toggle click
662668
toggle = MagicMock()
663669
toggle.get_attribute = AsyncMock(return_value="false")
664670
toggle.click = AsyncMock(side_effect=Exception("Click failed"))
665671

666-
label = MagicMock()
667-
label.click = AsyncMock()
668-
root = MagicMock()
669-
root.locator.return_value = label
672+
alt_toggle = MagicMock()
673+
alt_toggle.count = AsyncMock(return_value=1)
674+
alt_toggle.click = AsyncMock()
670675

671676
def locator_side_effect(selector):
672-
if "button" in selector and 'data-test-toggle="manual-budget"' in selector:
677+
if "button" in selector and "switch" in selector:
673678
return toggle
679+
if 'aria-label="Toggle thinking budget between auto and manual"' in selector:
680+
return alt_toggle
674681
if 'data-test-toggle="manual-budget"' in selector:
682+
root = MagicMock()
683+
root.locator.return_value = MagicMock()
675684
return root
676685
return toggle
677686

@@ -690,7 +699,7 @@ def locator_side_effect(selector):
690699
True, check_disconnect_mock
691700
)
692701

693-
label.click.assert_called()
702+
alt_toggle.click.assert_called()
694703

695704

696705
@pytest.mark.asyncio
@@ -1185,26 +1194,28 @@ async def test_control_thinking_mode_toggle_click_cancelled_error(
11851194
async def test_control_thinking_mode_toggle_fallback_success(
11861195
mock_controller, mock_page
11871196
):
1188-
"""Test successful fallback to label click (lines 459-468)."""
1197+
"""Test successful fallback to aria-label based toggle click."""
11891198
toggle = AsyncMock()
11901199
toggle.get_attribute = AsyncMock(side_effect=["false", "true"]) # Before and after
11911200
toggle.click = AsyncMock(side_effect=Exception("Click failed"))
11921201
toggle.scroll_into_view_if_needed = AsyncMock()
11931202

1194-
label = AsyncMock()
1195-
label.click = AsyncMock() # Success on fallback
1196-
1197-
root = MagicMock()
1198-
root.locator.return_value = label
1203+
alt_toggle = AsyncMock()
1204+
alt_toggle.count = AsyncMock(return_value=1)
1205+
alt_toggle.click = AsyncMock() # Success on fallback
11991206

12001207
locator_call_count = [0]
12011208

12021209
def locator_side_effect(selector):
12031210
locator_call_count[0] += 1
1204-
if locator_call_count[0] == 1: # First call for toggle button
1211+
if "button" in selector and "switch" in selector:
12051212
return toggle
1206-
else: # Second call for mat-slide-toggle root
1207-
return root
1213+
if 'aria-label="Toggle thinking mode"' in selector:
1214+
return alt_toggle
1215+
# Old fallback path
1216+
root = MagicMock()
1217+
root.locator.return_value = MagicMock()
1218+
return root
12081219

12091220
mock_page.locator.side_effect = locator_side_effect
12101221

@@ -1218,13 +1229,13 @@ def locator_side_effect(selector):
12181229
):
12191230
mock_controller._check_disconnect = AsyncMock()
12201231

1221-
# Should succeed via fallback (lines 459-468)
1232+
# Should succeed via fallback
12221233
result = await mock_controller._control_thinking_mode_toggle(
12231234
True, check_disconnect_mock
12241235
)
12251236

1226-
# Verify label click was called (fallback)
1227-
label.click.assert_called()
1237+
# Verify alt_toggle click was called (fallback)
1238+
alt_toggle.click.assert_called()
12281239
assert result is True
12291240

12301241

@@ -1429,26 +1440,25 @@ async def test_control_thinking_budget_toggle_click_cancelled_error(
14291440
async def test_control_thinking_budget_toggle_fallback_success(
14301441
mock_controller, mock_page
14311442
):
1432-
"""Test successful fallback to label click (lines 545-554)."""
1443+
"""Test successful fallback to aria-label based toggle click."""
14331444
toggle = AsyncMock()
14341445
toggle.get_attribute = AsyncMock(side_effect=["false", "true"]) # Before and after
14351446
toggle.click = AsyncMock(side_effect=Exception("Click failed"))
14361447
toggle.scroll_into_view_if_needed = AsyncMock()
14371448

1438-
label = AsyncMock()
1439-
label.click = AsyncMock() # Success on fallback
1440-
1441-
root = MagicMock()
1442-
root.locator.return_value = label
1443-
1444-
locator_call_count = [0]
1449+
alt_toggle = AsyncMock()
1450+
alt_toggle.count = AsyncMock(return_value=1)
1451+
alt_toggle.click = AsyncMock() # Success on fallback
14451452

14461453
def locator_side_effect(selector):
1447-
locator_call_count[0] += 1
1448-
if locator_call_count[0] == 1: # First call for toggle button
1454+
if "button" in selector and "switch" in selector:
14491455
return toggle
1450-
else: # Second call for mat-slide-toggle root
1451-
return root
1456+
if 'aria-label="Toggle thinking budget between auto and manual"' in selector:
1457+
return alt_toggle
1458+
# Old fallback path
1459+
root = MagicMock()
1460+
root.locator.return_value = MagicMock()
1461+
return root
14521462

14531463
mock_page.locator.side_effect = locator_side_effect
14541464

@@ -1462,13 +1472,13 @@ def locator_side_effect(selector):
14621472
):
14631473
mock_controller._check_disconnect = AsyncMock()
14641474

1465-
# Should succeed via fallback (lines 545-554)
1475+
# Should succeed via fallback
14661476
await mock_controller._control_thinking_budget_toggle(
14671477
True, check_disconnect_mock
14681478
)
14691479

1470-
# Verify label click was called (fallback)
1471-
label.click.assert_called()
1480+
# Verify alt_toggle click was called (fallback)
1481+
alt_toggle.click.assert_called()
14721482

14731483

14741484
@pytest.mark.asyncio

0 commit comments

Comments
 (0)