132 lines
6.3 KiB
Python
132 lines
6.3 KiB
Python
"""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']
|