xfa-layout-engine 1.0.0-beta.5

Box-model and pagination layout engine for XFA forms. Experimental — part of the PDFluent XFA stack, under active development.
Documentation
//! M1.6 — Integration test for the five trace-site emit helpers.
//!
//! Each helper in [`xfa_layout_engine::trace::sites`] is exercised under
//! a [`RecordingSink`] and we assert the resulting events carry the
//! expected phase, reason, and source-module hints. This proves the
//! emit pipeline (helper → `emit` → thread-local sink → record) end to
//! end without depending on engine-side wiring (which is intentionally
//! deferred — see `benchmarks/runs/M1_RESULT.md`).

use std::rc::Rc;
use xfa_layout_engine::trace::{sites, with_sink, Phase, Reason, RecordingSink, TraceEvent};

fn record_sites() -> Vec<TraceEvent> {
    let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
    with_sink(sink.clone(), || {
        sites::bind(
            "form1.subform",
            Reason::DataCountMatchesInitial,
            "3 instances created from 3 data records",
        );
        sites::occur("form1.subform.row", Reason::SubformMaterialisedFromData, 3);
        sites::presence(
            "form1.subform.field_b",
            Reason::PresenceHidden,
            "presence='hidden'",
        );
        sites::paginate(
            "form1.section_c",
            Reason::PaginateDeferToNextPageMinH,
            267.3,
            380.7,
        );
        sites::suppress(
            Reason::SuppressEmptyDataPageDropped,
            3,
            "drop empty data-bound page under form-DOM cap",
        );
    });
    sink.events()
}

#[test]
fn five_hot_phase_helpers_emit_distinct_events() {
    let events = record_sites();
    assert_eq!(events.len(), 5, "expected one event per helper call");

    let phases: Vec<_> = events.iter().map(|e| e.phase).collect();
    assert_eq!(
        phases,
        vec![
            Phase::Bind,
            Phase::Occur,
            Phase::Presence,
            Phase::Paginate,
            Phase::Suppress,
        ]
    );
}

#[test]
fn bind_event_carries_som_decision_and_source() {
    let events = record_sites();
    let e = &events[0];
    assert_eq!(e.phase, Phase::Bind);
    assert_eq!(e.reason, Reason::DataCountMatchesInitial);
    assert_eq!(e.som.as_deref(), Some("form1.subform"));
    assert!(e
        .decision
        .as_deref()
        .map(|d| d.contains("3 instances"))
        .unwrap_or(false));
    assert_eq!(e.source.as_deref(), Some("xfa_layout_engine::form::bind"));
}

#[test]
fn occur_event_carries_count_in_input() {
    let events = record_sites();
    let e = &events[1];
    assert_eq!(e.phase, Phase::Occur);
    assert_eq!(e.reason, Reason::SubformMaterialisedFromData);
    assert_eq!(e.input.as_deref(), Some("count=3"));
}

#[test]
fn presence_event_uses_presence_phase() {
    let events = record_sites();
    let e = &events[2];
    assert_eq!(e.phase, Phase::Presence);
    assert_eq!(e.reason, Reason::PresenceHidden);
    assert_eq!(e.som.as_deref(), Some("form1.subform.field_b"));
}

#[test]
fn paginate_event_carries_geometry_in_input() {
    let events = record_sites();
    let e = &events[3];
    assert_eq!(e.phase, Phase::Paginate);
    assert_eq!(e.reason, Reason::PaginateDeferToNextPageMinH);
    let input = e.input.as_deref().unwrap();
    assert!(input.contains("available_h=267.3000"));
    assert!(input.contains("needed_h=380.7000"));
}

#[test]
fn suppress_event_carries_page_index_in_input() {
    let events = record_sites();
    let e = &events[4];
    assert_eq!(e.phase, Phase::Suppress);
    assert_eq!(e.reason, Reason::SuppressEmptyDataPageDropped);
    assert_eq!(e.input.as_deref(), Some("page_index=3"));
}

#[test]
fn no_events_when_sink_uninstalled() {
    // The helpers must be silent (and not panic) when called outside a
    // with_sink scope. This is the "default off" guarantee.
    sites::bind("a", Reason::Unspecified, "x");
    sites::occur("a", Reason::Unspecified, 0);
    sites::presence("a", Reason::Unspecified, "x");
    sites::paginate("a", Reason::Unspecified, 0.0, 0.0);
    sites::suppress(Reason::Unspecified, 0, "x");
}

#[test]
fn canonical_json_of_recorded_events_is_byte_stable() {
    let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
    with_sink(sink.clone(), || {
        sites::bind("form1", Reason::DataCountMatchesInitial, "ok");
        sites::paginate("form1.x", Reason::PaginateSplit, 100.0, 50.0);
    });
    let a = sink.to_canonical_json();
    let b = sink.to_canonical_json();
    assert_eq!(a, b);
    assert!(a.contains("\"phase\": \"bind\""));
    assert!(a.contains("\"phase\": \"paginate\""));
}