"""Only click a freshly checked, fully painted expand control.""" import json import subprocess from pathlib import Path from types import SimpleNamespace import pytest from backend.browser import tools FIXTURE = Path(__file__).with_name('reply_dom_fixture_d41.cjs') def tree(*, clipped=False, viewport_edge=False): control = {'attrs': {'id':'expand','class':'show-more'}, 'text':'展开1条回复'} if viewport_edge: control['rect'] = {'left':0,'right':100,'top':585,'bottom':620,'width':100,'height':35} area = {'attrs': {'id':'reply-area','class':'reply-container'}, 'children':[control]} if clipped: area['style'] = {'overflowY':'auto'} area['rect'] = {'left':0,'right':300,'top':0,'bottom':40,'width':300,'height':40} root = {'attrs': {'id':'root-comment','class':'comment-item'}, 'children':[ {'attrs': {'id':'root-content','class':'content'},'text':'根评论'}]} return {'attrs': {'id':'noteContainer'}, 'children':[ {'attrs': {'id':'detail-desc'},'text':'正文'}, {'attrs': {'id':'thread','class':'parent-comment'},'children':[root,area]}]} def observe(code, arg, value): result = subprocess.run(['node',str(FIXTURE)], input=json.dumps({'code':code,'arg':arg,'tree':value}), capture_output=True,text=True,check=True,timeout=5) return json.loads(result.stdout) @pytest.mark.parametrize('options', [{'clipped':True},{'viewport_edge':True}]) def test_partial_control_can_use_an_actually_painted_point_without_implicit_scroll(options): value = tree(**options) inspected = observe(tools._INSPECT_SOURCE_CANDIDATES, {'max_comments':5}, value) assert inspected['reply_controls'][0]['in_view'] is True checked = observe(tools._OBSERVE_REPLY_CONTROL, {'selector':'#expand'}, value) assert checked['click_point']['y'] < (600 if options.get('viewport_edge') else 40) def test_full_control_has_a_program_observed_native_point(): result = observe(tools._OBSERVE_REPLY_CONTROL, {'selector':'#expand'}, tree()) assert result['click_point'] == {'x':150,'y':27,'viewport_width':400,'viewport_height':600} def test_real_occlusion_is_rejected_by_paint_hit_testing(): value=tree();value['occluded']=True result=observe(tools._OBSERVE_REPLY_CONTROL,{'selector':'#expand'},value) assert result['error']=='reply_control_occluded' async def test_changed_control_is_not_clicked_after_reading_candidates(tmp_path,monkeypatch): tid = 'current-visible-point-d42' tools.clear_task_context(tid) tools.set_prospecting_context(tid) monkeypatch.setattr(tools,'task_doc_dir',lambda _:tmp_path) class Page: observed = 0 clicks = 0 async def get_url(self):return 'https://www.xiaohongshu.com/explore/'+'a'*24 async def evaluate(self,code,arg=None): if code==tools._OBSERVE_REPLY_CONTROL: self.observed+=1 return {'url':await self.get_url(),'fingerprint':str(self.observed),'owner_identity':'stable-root', 'owner_selector':'#thread','reply_count':0,'visible_reply_selectors':[], 'reply_snapshot_complete':True,'click_point':{'x':150,'y':27,'viewport_width':400,'viewport_height':600}} assert code==tools._INSPECT_SOURCE_CANDIDATES return {'url':await self.get_url(),'candidates':[]} async def get_elements_by_css_selector(self,_selector):return [SimpleNamespace()] @property async def mouse(self): async def click(**_point):self.clicks+=1 return SimpleNamespace(click=click) page=Page() async def current():return page with pytest.raises(ValueError,match='点击前回复入口或讨论已变化'): await tools._expand_replies(tid,SimpleNamespace(get_current_page=current),'#expand') assert page.clicks==0 tools.clear_task_context(tid)