Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
432 changes: 216 additions & 216 deletions examples/hr4366_committee_vs_floor_xml_diff.html

Large diffs are not rendered by default.

7,276 changes: 3,638 additions & 3,638 deletions examples/hr4366_house_vs_senate_xml_diff.html

Large diffs are not rendered by default.

572 changes: 286 additions & 286 deletions examples/hr8752_pdf_diff.html

Large diffs are not rendered by default.

598 changes: 299 additions & 299 deletions examples/hr8752_xml_diff.html

Large diffs are not rendered by default.

301 changes: 170 additions & 131 deletions src/deltatrack/formatters/diff_html.py

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions tests/test_corpus_tree_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
from deltatrack.bill_tree import extract_text_content, find_bill_bodies, find_bill_body, normalize_bill
from deltatrack.diff_bill import extract_amounts
from deltatrack.formatters.canonical import _pdf_tree_payload
from deltatrack.formatters.diff_html import _build_toc_from_tree
from deltatrack.formatters.diff_html import _build_tree_nav
from deltatrack.formatters.text_serializer import _xml_tree_payload, serialize_tree_for_tree
from deltatrack.parsers.pdf_anchors import extract_anchors
from deltatrack.parsers.pdf_text import pdf_full_text
Expand Down Expand Up @@ -256,8 +256,8 @@ def _assert_schema_and_levels(roots: list[dict]) -> None:
def _assert_no_blank_toc_rows(roots: list[dict], full_text: str) -> None:
"""Invariant 4: the leveled TOC the tree renders has no blank clickable rows
and no empty collapsible groups (the consumed-output blank-row check)."""
html = _build_toc_from_tree(roots, full_text)
leaves = re.findall(r'<li class="toc-child">(.*?)</li>', html, re.S)
html = _build_tree_nav(roots, full_text)
leaves = re.findall(r'<li class="tree-node"[^>]*>(.*?)</li>', html, re.S)
blank_leaves = [leaf for leaf in leaves if not re.sub(r"<[^>]+>", "", leaf).strip()]
assert not blank_leaves, f"{len(blank_leaves)} blank TOC leaf row(s)"
summaries = re.findall(r"<summary[^>]*>(.*?)</summary>", html, re.S)
Expand All @@ -268,7 +268,7 @@ def _assert_no_blank_toc_rows(roots: list[dict], full_text: str) -> None:
# blank rows — passing the checks above while rendering nothing. If the tree
# carries any labeled node, the TOC must render at least one entry.
if any((n["label"] or "").strip() for n in _walk(roots)):
assert "toc-child" in html or "toc-group" in html, "labeled tree rendered an empty TOC"
assert "tree-node" in html or "tree-group" in html, "labeled tree rendered an empty TOC"


def _assert_zero_anchor_layout(path: Path, test_id: str, full_text: str, anchors: tuple, offsets: dict) -> None:
Expand Down
6 changes: 3 additions & 3 deletions tests/test_diff_html_node_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_cards_keep_original_change_order_indices_under_grouping():
def test_cards_render_flat_when_no_change_has_a_node_path():
view = _view([_change(), _change()])
html = _cards_section_html(view)
assert "card-group" not in html
assert "change-group" not in html
assert html.find('id="change-0"') < html.find('id="change-1"')


Expand Down Expand Up @@ -127,10 +127,10 @@ def test_degraded_card_without_group_label_lands_in_uncategorized():
def test_filter_js_hides_empty_card_groups():
# Pin the hide logic, not just the selector: applyFilters must toggle a
# card group's display off when none of its cards survive the filter.
block_start = _JS.find(".card-group")
block_start = _JS.find(".change-group")
assert block_start != -1
block = _JS[block_start : block_start + 400]
assert ".change-card" in block and "display = vis === 0 ? 'none' : ''" in block
assert ".change" in block and "display = vis === 0 ? 'none' : ''" in block


def test_group_labels_are_escaped_in_cards_and_sidebar():
Expand Down
4 changes: 2 additions & 2 deletions tests/test_diff_pdf_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ def test_writes_html_file(self, tmp_path):
assert "reported-in-house" in html
assert "engrossed-in-house" in html
# Delegating to compare_pdfs_html means the report carries the
# full-bill view + embedded export, not just the changed-section cards.
assert "full-bill" in html
# full-text view + embedded export, not just the changed-section cards.
assert "full-text" in html
assert "diff.json" in html

def test_stdout_when_no_output(self, capsys):
Expand Down
13 changes: 11 additions & 2 deletions tests/test_financial_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ class TestSectionsWhoseOnlyChangeIsMoney:
def change_cards():
"""The report's change cards, as (breadcrumb heading, card markup) pairs.

Scoped to the changes view deliberately. The report also carries a full-bill
Scoped to the changes view deliberately. The report also carries a full-text
view, which renders every section whether or not it changed, so asserting a
section name or an amount against the whole document passes with the defect
present -- verified: the first draft of these tests did exactly that and was
Expand All @@ -751,8 +751,17 @@ def change_cards():
later = [pos for pos, _ in views if pos > start]
changes_view = html[start : later[0] if later else len(html)]

# Split on the card's id, not its class. `change` is a prefix of `change-type`,
# `change-group`, `change__header` and `change__body`, so matching the class
# alone shatters one card into fragments and an amount lands in a different
# chunk from the heading that scopes it -- which reads as the amount being
# absent from the report, exactly the defect these tests exist to catch.
# Only a card carries `id="change-<n>"`.
starts = [m.start() for m in re.finditer(r'<div class="change[^"]*" id="change-\d+"', changes_view)]
bounds = starts + [len(changes_view)]
cards = []
for chunk in changes_view.split('class="change-card')[1:]:
for i, pos in enumerate(starts):
chunk = changes_view[pos : bounds[i + 1]]
heading = re.search(r"<h3>(.*?)</h3>", chunk, re.S)
cards.append((heading.group(1) if heading else "", chunk))
return cards
Expand Down
4 changes: 2 additions & 2 deletions tests/test_formatter_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def test_a_pair_at_or_above_the_cutoff_renders_inline(old_text: str, new_text: s
assert _text.word_diff(old_text, new_text, threshold=0.0) is not None # sanity: a diff exists
assert _ratio(old_text, new_text) == ratio, "fixture drifted off its intended side of the cutoff"
html = _prose_card(old_text, new_text)
assert '<div class="change-body diff-inline">' in html
assert '<div class="change__body diff-inline">' in html
assert '<div class="old-text">' not in html


Expand All @@ -246,7 +246,7 @@ def test_a_pair_immediately_below_the_cutoff_renders_stacked():
html = _prose_card(BELOW_OLD, BELOW_NEW)
assert '<div class="old-text">' in html
assert '<div class="new-text">' in html
assert '<div class="change-body diff-inline">' not in html
assert '<div class="change__body diff-inline">' not in html


def _ratio(old_text: str, new_text: str) -> float:
Expand Down
22 changes: 11 additions & 11 deletions tests/test_formatters_diff_html_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ def _change(**overrides) -> ChangeView:

def test_basic_card_structure():
html = _build_card(_change(old_text="old prose", new_text="new prose"), 0)
assert html.startswith('<div class="change-card modified" id="change-0" data-type="modified">')
assert html.startswith('<div class="change" id="change-0" data-type="modified">')
assert html.rstrip().endswith("</div>")
assert '<span class="badge badge-modified">modified</span>' in html
assert '<span class="change-type" data-type="modified">modified</span>' in html
assert "<h3>TITLE I &gt; Customs</h3>" in html


Expand Down Expand Up @@ -62,9 +62,9 @@ def test_citation_block_present_when_provided():
html = _build_card(_change(citation_html=citation, old_text="a", new_text="b"), 0)
assert citation in html
# Citation sits between header close and body.
h_close = html.index("</div>", html.index('class="change-header"'))
h_close = html.index("</div>", html.index('class="change__header"'))
cite_pos = html.index('class="citation"')
body_pos = html.index("change-body")
body_pos = html.index("change__body")
assert h_close < cite_pos < body_pos


Expand All @@ -83,13 +83,13 @@ def test_degraded_card_adds_unanchored_class_and_h3_class():
),
0,
)
assert '<div class="change-card modified unanchored"' in html
assert '<div class="change unanchored"' in html
assert '<h3 class="degraded">' in html


def test_added_card_body_uses_added_text_class():
html = _build_card(_change(change_type="added", new_text="brand new clause"), 0)
assert '<div class="change-body added-text">brand new clause</div>' in html
assert '<div class="change__body added-text">brand new clause</div>' in html


def test_added_card_escapes_new_text():
Expand All @@ -100,7 +100,7 @@ def test_added_card_escapes_new_text():

def test_removed_card_body_uses_removed_text_class():
html = _build_card(_change(change_type="removed", old_text="old clause"), 0)
assert '<div class="change-body removed-text">old clause</div>' in html
assert '<div class="change__body removed-text">old clause</div>' in html


def test_modified_card_uses_inline_word_diff_when_similar():
Expand All @@ -112,7 +112,7 @@ def test_modified_card_uses_inline_word_diff_when_similar():
),
0,
)
assert '<div class="change-body diff-inline">' in html
assert '<div class="change__body diff-inline">' in html
assert "<del>$1,000,000</del>" in html
assert "<ins>$2,500,000</ins>" in html

Expand All @@ -126,7 +126,7 @@ def test_modified_card_falls_back_to_stacked_when_dissimilar():
),
0,
)
assert '<div class="change-body">' in html
assert '<div class="change__body">' in html
assert '<div class="old-text">' in html
assert '<div class="new-text">' in html

Expand All @@ -146,7 +146,7 @@ def test_moved_card_renders_move_info_then_body():
)
assert move_html in html
# move-info appears before the body.
body_pos = html.index("change-body")
body_pos = html.index("change__body")
move_pos = html.index('class="move-info"')
assert move_pos < body_pos

Expand All @@ -165,7 +165,7 @@ def test_moved_card_unchanged_body_renders_single_body_div():
# Single body div, not stacked.
assert "old-text" not in html
assert "new-text" not in html
assert '<div class="change-body">same</div>' in html
assert '<div class="change__body">same</div>' in html


def test_moved_card_with_word_diff_fallback_uses_stacked():
Expand Down
46 changes: 23 additions & 23 deletions tests/test_formatters_diff_html_fullbill.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Tests for the full-bill tracked-changes view + Changes/Full toggle.
"""Tests for the full-text tracked-changes view + Changes/Full toggle.

The renderer takes one canonical document and builds its own view from it
(DeltaTrack#653), so these tests hand it a document and nothing else. A document
carrying full text gets the toggle, the full-bill pane and the embed; a document
carrying full text gets the toggle, the full-text pane and the embed; a document
without it (metadata only) renders the change cards alone, which the
no-full-text tests pin.
"""
Expand Down Expand Up @@ -52,7 +52,7 @@ def _change(change_id: str, change_type: str, **fields) -> dict:

def _no_full_text() -> dict:
"""A document carrying no full text — the report the renderer produces without
a full-bill pane, an embed, find, navigation or export."""
a full-text pane, an embed, find, navigation or export."""
return {
"schema_version": "3.0",
"bill": _BILL,
Expand Down Expand Up @@ -102,10 +102,10 @@ def _canonical() -> dict:


def test_no_full_bill_ui_without_full_text_but_document_still_embedded():
"""No full-bill UI without full text — but the document is embedded regardless.
"""No full-text UI without full text — but the document is embedded regardless.

Two separate rules, and conflating them is a live regression risk. The
*controls* (toggle, full-bill pane, find, navigation, export) are gated on
*controls* (toggle, full-text pane, find, navigation, export) are gated on
full text because none of them has anything to act on without it. The
*payload* is not gated on anything: the report carries the diff document it
was rendered from, which is what makes a standalone report self-describing
Expand All @@ -123,7 +123,7 @@ def test_no_full_bill_ui_without_full_text_but_document_still_embedded():
document = _no_full_text()
html = format_diff_html(document)

# No full-bill UI: nothing to drive it.
# No full-text UI: nothing to drive it.
assert "data-view=" not in html
assert 'class="view view-full"' not in html

Expand Down Expand Up @@ -188,7 +188,7 @@ def test_treeless_canonical_renders_the_toc_empty_state():
same string for an empty list.
"""
html = format_diff_html(_canonical())
assert 'class="sidebar-toc"' in html
assert 'class="sidebar-tree"' in html
assert "No sections detected." in html


Expand All @@ -206,18 +206,18 @@ def test_full_bill_rows_carry_no_orphan_section_ids():

def test_no_toc_without_full_text():
html = format_diff_html(_no_full_text())
assert 'class="sidebar-toc"' not in html
assert 'class="sidebar-tree"' not in html


def test_added_and_modified_marks_projected():
html = format_diff_html(_canonical())
# Added: just an <ins> around the v2 slice.
assert '<ins class="diff-add" id="attr-c-1">ADD0</ins>' in html
assert '<ins class="diff-added" id="attr-c-1">ADD0</ins>' in html
# Modified: new text highlighted in place; old text is not shown inline (it
# lives in the Changes cards), so "old1" never reaches the full-bill view.
assert '<span class="diff-mod" id="attr-c-2"' in html
# lives in the Changes cards), so "old1" never reaches the full-text view.
assert '<span class="diff-modified" id="attr-c-2"' in html
assert ">MOD1</span>" in html
assert '<del class="diff-del">old1</del>' not in html # old text not rendered inline
assert '<del class="diff-removed">old1</del>' not in html # old text not rendered inline
assert "fb-del-row" not in html
# Untouched tail text remains.
assert "KEEP" in html
Expand Down Expand Up @@ -265,43 +265,43 @@ def test_xml_full_bill_gutterless_no_truncation():
assert '">ENT OF DEFENSE' not in html
assert '">y construction' not in html
# Gutterless mode: no synthesized line numbers, no PDF page markers.
assert 'class="fb-page"' not in html
assert '<span class="fb-gutter">' not in html
assert 'class="full-text-page"' not in html
assert '<span class="full-text-line__number">' not in html
# The modified span is still highlighted in place with the new text.
assert '<span class="diff-mod" id="attr-x-1"' in html
assert '<span class="diff-modified" id="attr-x-1"' in html


def test_full_bill_rows_carry_line_number_gutter():
"""Each source line renders as a row with its line number in the gutter."""
html = format_diff_html(_canonical())
# Page marker precedes the rows; line numbers sit in the gutter column.
assert '<div class="fb-page">p. 1</div>' in html
assert '<span class="fb-gutter">1</span>' in html
assert '<span class="fb-gutter">3</span>' in html
assert '<div class="full-text-page">p. 1</div>' in html
assert '<span class="full-text-line__number">1</span>' in html
assert '<span class="full-text-line__number">3</span>' in html
# The readable text column carries the content without the gutter prefix.
assert '<span class="fb-text"><ins class="diff-add" id="attr-c-1">ADD0</ins></span>' in html
assert '<span class="full-text-line__text"><ins class="diff-added" id="attr-c-1">ADD0</ins></span>' in html


def test_modified_highlighted_in_place_without_old_text():
"""A modified change highlights its new text in place; the old text stays in
the Changes cards (not echoed into the full-bill view)."""
the Changes cards (not echoed into the full-text view)."""
html = format_diff_html(_canonical())
assert 'title="modified — see Changes for the old text"' in html
assert "fb-del-row" not in html
assert '<del class="diff-del">old1</del>' not in html
assert '<del class="diff-removed">old1</del>' not in html


def test_removed_appendix_lists_removals():
html = format_diff_html(_canonical())
assert 'class="removed-appendix"' in html
assert 'class="removed-changes"' in html
assert "TITLE I &gt; SEC 2" in html
# The removed v1 slice is shown struck through.
assert "GONE" in html


def test_meta_accounts_for_placed_and_removed():
html = format_diff_html(_canonical())
meta = re.search(r'<div class="full-bill-meta">(.*?)</div>', html).group(1)
meta = re.search(r'<div class="full-text-meta">(.*?)</div>', html).group(1)
assert "2 of 3 changes shown inline" in meta
assert "1 removed below" in meta

Expand Down
2 changes: 1 addition & 1 deletion tests/test_formatters_diff_html_sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def test_nav_item_basic():
item = _build_nav_item(_change(), 0)
assert item.startswith('<li class="nav-item" data-type="modified">')
assert 'href="#change-0"' in item
assert '<span class="badge badge-modified">modified</span>' in item
assert '<span class="change-type" data-type="modified">modified</span>' in item
assert "TITLE I &gt; Customs" in item


Expand Down
10 changes: 5 additions & 5 deletions tests/test_formatters_diff_html_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ def test_summary_bar_canonical_order():
Asserts ordering by checking byte position.
"""
html = format_diff_html(_empty(summary={"modified": 5, "added": 3, "removed": 2, "moved": 1}))
# Find each badge marker and confirm ascending positions.
pos_modified = html.find('class="badge badge-modified"')
pos_added = html.find('class="badge badge-added"')
pos_removed = html.find('class="badge badge-removed"')
pos_moved = html.find('class="badge badge-moved"')
# Find each change-type marker and confirm ascending positions.
pos_modified = html.find('class="change-type" data-type="modified"')
pos_added = html.find('class="change-type" data-type="added"')
pos_removed = html.find('class="change-type" data-type="removed"')
pos_moved = html.find('class="change-type" data-type="moved"')
assert -1 < pos_modified < pos_added < pos_removed < pos_moved


Expand Down
6 changes: 3 additions & 3 deletions tests/test_front_matter_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from deltatrack.compare.pdf import compare_pdfs
from deltatrack.compare.xml import compare_xml, compare_xml_html
from deltatrack.diff_bill import extract_amounts
from deltatrack.formatters.diff_html import _build_toc_from_tree
from deltatrack.formatters.diff_html import _build_tree_nav
from deltatrack.parsers.pdf_anchors import Anchor
from deltatrack.structure_tree import TreeNode, build_pdf_tree, build_xml_tree
from tests.corpus_paths import FIXTURES_DIR, fixture_path
Expand Down Expand Up @@ -201,7 +201,7 @@ def test_toc_front_matter_renders_as_leaf_when_no_labeled_children() -> None:
"full_text_span": {"start": 0, "end": 5},
"children": [],
}
html = _build_toc_from_tree([_fm_node([boilerplate])], full_text="A BILL\nmaking\n")
html = _build_tree_nav([_fm_node([boilerplate])], full_text="A BILL\nmaking\n")
assert FRONT_MATTER_LABEL in html
assert "<details" not in html, "no empty toggle — a leaf jump to the opening"

Expand All @@ -215,7 +215,7 @@ def test_toc_front_matter_renders_as_toggle_with_labeled_children() -> None:
"full_text_span": {"start": 6, "end": 9},
"children": [],
}
html = _build_toc_from_tree([_fm_node([section])], full_text="A BILL\nmaking\n")
html = _build_tree_nav([_fm_node([section])], full_text="A BILL\nmaking\n")
assert "<details" in html and "Definitions" in html, "a real section gives it a toggle"


Expand Down
Loading