"""Run the real source observers against structural discussion variants.""" import json import subprocess from pathlib import Path import pytest from backend.browser import tools FIXTURE = Path(__file__).with_name('reply_dom_fixture_d41.cjs') def element(classes='', identity='', text='', children=None, **attributes): return {'attrs': {'class': classes, **({'id': identity} if identity else {}), **attributes}, 'text': text, 'children': children or []} def speaker(identity, kind='comment', text='本人需求', *, nested_text=True, extra=None, **attributes): content = element('content', identity + '-content', children=[element('note-text', identity + '-text', text)] if nested_text else [], text='' if nested_text else text) author = {'tag': 'a', 'attrs': {'class': 'name', 'href': '/user/profile/' + 'b' * 24}, 'text': '同名发言人'} return element('comment-item' if kind == 'comment' else 'comment-item-sub', identity, children=[element('author-wrapper', children=[author]), content, *(extra or [])], **{'data-comment-id': identity + '-key', **attributes}) def note(*threads): return element('', 'noteContainer', children=[element('', 'detail-desc', '正文'), *threads]) def thread(root, *replies, identity='thread'): return element('parent-comment', identity, children=[root, element('reply-container', children=list(replies))]) def observe(tree, identity, code=None): request = {'code': code or tools._OBSERVE_SOURCE, 'arg': '#' + identity, 'tree': tree} result = subprocess.run(['node', str(FIXTURE)], input=json.dumps(request, ensure_ascii=False), text=True, capture_output=True, check=True, timeout=5) return json.loads(result.stdout) def inspect(tree): request = {'code': tools._INSPECT_SOURCE_CANDIDATES, 'arg': {'max_comments': 5, 'saved_selectors': []}, 'tree': tree} result = subprocess.run(['node', str(FIXTURE)], input=json.dumps(request, ensure_ascii=False), text=True, capture_output=True, check=True, timeout=5) return json.loads(result.stdout) def test_nested_content_aliases_use_one_canonical_source_and_preserve_nested_parent(): reply = speaker('reply', 'reply', '回复自己的需求') root = speaker('root', extra=[reply]) tree = note(root) observed = observe(tree, 'reply-content') assert observed['source_type'] == 'reply' assert observed['text'] == '回复自己的需求' assert observed['parent_source_selector'] == observed['root_source_selector'] == '#root-content' assert [item['source_selector'] for item in inspect(tree)['candidates']] == ['#detail-desc', '#root-content', '#reply-content'] def test_sibling_thread_direct_reply_links_only_its_own_root_even_with_duplicate_names(): first = thread(speaker('root-one'), speaker('reply-one', 'reply'), identity='thread-one') second = thread(speaker('root-two'), speaker('reply-two', 'reply'), identity='thread-two') observed = observe(note(first, second), 'reply-two-content') assert observed['parent_source_selector'] == observed['root_source_selector'] == '#root-two-content' def test_nested_reply_keeps_immediate_parent_and_original_root(): child = speaker('child', 'reply') reply = speaker('reply', 'reply', extra=[child]) root = speaker('root', extra=[reply]) observed = observe(note(root), 'child-content') assert observed['parent_source_selector'] == '#reply-content' assert observed['root_source_selector'] == '#root-content' @pytest.mark.parametrize('marker', [ element('reply-to', text='其他发言人'), element('reply-to-name', text='同名发言人'), element('', text='其他发言人', **{'data-reply-to': 'somebody'}), ]) def test_flat_reply_target_without_a_comment_reference_keeps_parent_unknown(marker): reply = speaker('reply', 'reply', extra=[marker]) observed = observe(note(thread(speaker('root'), reply)), 'reply-content') assert observed['root_source_selector'] == '#root-content' assert observed['parent_source_selector'] == '' def test_flat_plain_reply_prefix_does_not_guess_target_by_name(): observed = observe(note(thread(speaker('root'), speaker('reply', 'reply', '回复 同名发言人:自己的话'))), 'reply-content') assert observed['root_source_selector'] == '#root-content' assert observed['parent_source_selector'] == '' def test_flat_reply_explicit_comment_reference_can_resolve_another_reply(): first = speaker('first', 'reply') second = speaker('second', 'reply', extra=[element('reply-to', text='同名发言人')], **{'data-parent-comment-id': 'first-key'}) observed = observe(note(thread(speaker('root'), first, second)), 'second-content') assert observed['parent_source_selector'] == '#first-content' assert observed['root_source_selector'] == '#root-content' @pytest.mark.parametrize('attributes', [ {'data-parent-comment-id': 'other-thread-key'}, {'data-parent-comment-id': 'root-key', 'data-parent-id': 'other-key'}, ]) def test_unmatched_or_conflicting_parent_reference_does_not_fall_back_to_root(attributes): observed = observe(note(thread(speaker('root'), speaker('reply', 'reply', **attributes))), 'reply-content') assert observed['root_source_selector'] == '#root-content' assert observed['parent_source_selector'] == '' def test_two_root_records_in_a_thread_are_ambiguous(): ambiguous = element('parent-comment', 'thread', children=[speaker('root-one'), speaker('root-two'), element('reply-container', children=[speaker('reply', 'reply')])]) observed = observe(note(ambiguous), 'reply-content') assert observed['parent_source_selector'] == observed['root_source_selector'] == '' def test_unlabelled_sibling_layout_does_not_invent_a_thread(): ambiguous = element('comments-container', children=[speaker('root'), element('reply-container', children=[speaker('reply', 'reply')])]) observed = observe(note(ambiguous), 'reply-content') assert observed['parent_source_selector'] == observed['root_source_selector'] == '' def test_independent_content_blocks_are_omitted_instead_of_arbitrarily_selecting_one(): root = speaker('root', extra=[element('note-text', 'other-content', '另一段独立内容')]) candidates = inspect(note(root))['candidates'] assert [item['source_type'] for item in candidates] == ['note']