use xfa_layout_engine::trace::{sites as trace_sites, Reason};
use super::registry::AdobeCompatRuleId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CapDecision {
pub max_suppress: usize,
pub rule: AdobeCompatRuleId,
}
impl CapDecision {
pub const fn uncapped() -> Self {
Self {
max_suppress: usize::MAX,
rule: AdobeCompatRuleId::CapSuppressionByFormDomPageAreaCount,
}
}
pub const fn capped_at(max_suppress: usize) -> Self {
Self {
max_suppress,
rule: AdobeCompatRuleId::CapSuppressionByFormDomPageAreaCount,
}
}
}
pub fn cap_suppression_by_form_dom(
layout_pages: usize,
form_dom_page_count: Option<usize>,
) -> CapDecision {
match form_dom_page_count {
Some(target) if target < layout_pages => {
let max = layout_pages - target;
trace_sites::suppress(
Reason::SuppressCappedByFormDom,
u32::try_from(target).unwrap_or(u32::MAX),
format!("form_dom_pages={target} layout_pages={layout_pages} max_suppress={max}"),
);
CapDecision::capped_at(max)
}
Some(target) => {
trace_sites::suppress(
Reason::SuppressCappedByFormDom,
u32::try_from(target).unwrap_or(u32::MAX),
format!("form_dom_pages={target} layout_pages={layout_pages} max_suppress=0"),
);
CapDecision::capped_at(0)
}
None => CapDecision::uncapped(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SuppressPreflightDecision {
pub run_suppression: bool,
pub rule: AdobeCompatRuleId,
}
pub fn suppress_empty_pages_only_when_real_data_bound(
layout_pages: usize,
any_data_bound: bool,
) -> SuppressPreflightDecision {
let run = layout_pages > 1 && any_data_bound;
let reason = if run {
Reason::SuppressGatedByDataBoundSignal
} else {
Reason::SuppressSkippedNoDataBoundSignal
};
trace_sites::suppress(
reason,
u32::try_from(layout_pages).unwrap_or(u32::MAX),
format!(
"layout_pages={layout_pages} any_data_bound={any_data_bound} run_suppression={run}"
),
);
SuppressPreflightDecision {
run_suppression: run,
rule: AdobeCompatRuleId::SuppressEmptyPagesOnlyWhenRealDataBound,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindNoneClassification {
DataField,
ExcludedBindNone,
ExcludedNonDataWidget,
}
pub fn exclude_bind_none_fields_from_page_data_suppression(
is_field_node: bool,
is_non_data_widget: bool,
data_bind_none: bool,
) -> BindNoneClassification {
if !is_field_node {
return BindNoneClassification::DataField;
}
if is_non_data_widget {
return BindNoneClassification::ExcludedNonDataWidget;
}
if data_bind_none {
return BindNoneClassification::ExcludedBindNone;
}
BindNoneClassification::DataField
}
pub fn emit_bind_none_summary(excluded_count: usize) {
if excluded_count == 0 {
return;
}
trace_sites::suppress(
Reason::BindNoneFieldExcludedFromDataCheck,
u32::try_from(excluded_count).unwrap_or(u32::MAX),
format!("excluded_count={excluded_count}"),
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StaticTrimDecision {
pub allow_trim: bool,
pub single_page_collapse: bool,
pub rule: AdobeCompatRuleId,
}
pub fn static_xfaf_excess_page_trim_with_form_dom_guard(
is_static_form: bool,
template_has_dynamic_logic: bool,
n_layout: usize,
form_dom_page_count: Option<usize>,
) -> StaticTrimDecision {
let rule_id =
AdobeCompatRuleId::TrimStaticXfafExcessPagesWhenLayoutIsSinglePageAndFormDomAllows;
if !is_static_form {
return StaticTrimDecision {
allow_trim: false,
single_page_collapse: false,
rule: rule_id,
};
}
let single_page_collapse = !template_has_dynamic_logic && n_layout == 1;
let allow_trim = match form_dom_page_count {
Some(fdp) => fdp <= n_layout || single_page_collapse,
None => false,
};
let reason = if allow_trim {
Reason::StaticXfafTrimAllowed
} else {
Reason::StaticXfafTrimBlocked
};
trace_sites::suppress(
reason,
u32::try_from(n_layout).unwrap_or(u32::MAX),
format!(
"is_static={is_static_form} dynamic_logic={template_has_dynamic_logic} n_layout={n_layout} form_dom={form_dom_page_count:?} single_page_collapse={single_page_collapse} allow_trim={allow_trim}"
),
);
StaticTrimDecision {
allow_trim,
single_page_collapse,
rule: rule_id,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvisibleBindingDecision {
AcceptedVisible,
IgnoredInvisible,
}
pub fn ignore_invisible_server_metadata_bindings(presence_attr: &str) -> InvisibleBindingDecision {
if matches!(presence_attr, "invisible" | "hidden" | "inactive") {
InvisibleBindingDecision::IgnoredInvisible
} else {
InvisibleBindingDecision::AcceptedVisible
}
}
pub fn emit_invisible_binding_summary(ignored_count: usize) {
if ignored_count == 0 {
return;
}
trace_sites::bind(
"root",
Reason::InvisibleFieldBindingIgnored,
format!("ignored_count={ignored_count}"),
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WidgetClassification {
DataField,
ExcludedNonDataWidget,
}
pub fn exclude_non_data_widgets_from_page_suppression(
field_kind_is_non_data_widget: bool,
) -> WidgetClassification {
if field_kind_is_non_data_widget {
WidgetClassification::ExcludedNonDataWidget
} else {
WidgetClassification::DataField
}
}
pub fn emit_non_data_widget_summary(excluded_count: usize) {
if excluded_count == 0 {
return;
}
trace_sites::suppress(
Reason::NonDataWidgetExcludedFromDataCheck,
u32::try_from(excluded_count).unwrap_or(u32::MAX),
format!("excluded_count={excluded_count}"),
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OccurClampOutcome {
ClampedByMax,
LiftedByMin,
PassthroughInRange,
}
pub fn repeating_subform_instance_count_clamped_to_occur_range(
som: &str,
data_count: u32,
occur_min: u32,
occur_max: Option<u32>,
) -> (u32, OccurClampOutcome) {
let effective_max = occur_max.unwrap_or(data_count).max(occur_min);
let count = data_count.clamp(occur_min, effective_max);
let outcome = if data_count > effective_max {
OccurClampOutcome::ClampedByMax
} else if data_count < occur_min {
OccurClampOutcome::LiftedByMin
} else {
OccurClampOutcome::PassthroughInRange
};
match outcome {
OccurClampOutcome::ClampedByMax => {
trace_sites::occur(som, Reason::DataCountClampedByOccurMax, count as i64);
}
OccurClampOutcome::LiftedByMin => {
trace_sites::occur(som, Reason::DataCountLiftedByOccurMin, count as i64);
}
OccurClampOutcome::PassthroughInRange => {
}
}
(count, outcome)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindNoneExpansionGate {
Blocked,
Allowed,
}
pub fn bind_none_subform_does_not_auto_expand(
som: &str,
occur_is_repeating: bool,
name_is_empty: bool,
bind_none: bool,
) -> BindNoneExpansionGate {
if bind_none {
if occur_is_repeating && !name_is_empty {
trace_sites::occur(som, Reason::BindNoneSubformExpansionSkipped, 1);
}
BindNoneExpansionGate::Blocked
} else {
BindNoneExpansionGate::Allowed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FormDomReplicationDecision {
pub clones_to_add: usize,
}
pub fn form_dom_driven_repeat_instance_replication(
som: &str,
form_dom_instance_count: usize,
template_default_count: usize,
) -> FormDomReplicationDecision {
let clones_to_add =
if form_dom_instance_count > template_default_count && template_default_count > 0 {
form_dom_instance_count - template_default_count
} else {
0
};
if clones_to_add > 0 {
trace_sites::occur(
som,
Reason::SubformMaterialisedFromData,
form_dom_instance_count as i64,
);
}
FormDomReplicationDecision { clones_to_add }
}
#[cfg(test)]
mod tests {
use super::*;
use std::rc::Rc;
use xfa_layout_engine::trace::{with_sink, RecordingSink};
#[test]
fn no_form_dom_is_uncapped() {
let decision = cap_suppression_by_form_dom(7, None);
assert_eq!(decision.max_suppress, usize::MAX);
assert_eq!(
decision.rule,
AdobeCompatRuleId::CapSuppressionByFormDomPageAreaCount
);
}
#[test]
fn form_dom_smaller_than_layout_caps_to_difference() {
let decision = cap_suppression_by_form_dom(10, Some(7));
assert_eq!(decision.max_suppress, 3);
}
#[test]
fn form_dom_at_or_above_layout_caps_to_zero() {
let decision = cap_suppression_by_form_dom(7, Some(17));
assert_eq!(decision.max_suppress, 0);
}
#[test]
fn form_dom_equal_to_layout_caps_to_zero() {
let decision = cap_suppression_by_form_dom(7, Some(7));
assert_eq!(decision.max_suppress, 0);
}
#[test]
fn trace_anchor_fires_when_form_dom_present() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = cap_suppression_by_form_dom(7, Some(17));
});
let events = sink.events();
assert_eq!(events.len(), 1, "expected one trace event");
assert_eq!(events[0].phase.tag(), "suppress");
assert_eq!(events[0].reason.tag(), "suppress_capped_by_form_dom");
let decision = events[0].decision.as_deref().unwrap_or("");
assert!(
decision.contains("form_dom_pages=17"),
"decision should report form_dom_pages: {decision}"
);
assert!(
decision.contains("max_suppress=0"),
"decision should report max_suppress: {decision}"
);
}
#[test]
fn trace_anchor_silent_when_no_form_dom() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = cap_suppression_by_form_dom(7, None);
});
assert!(
sink.events().is_empty(),
"rule must not emit when form DOM is absent"
);
}
#[test]
fn suppress_preflight_allows_when_multi_page_and_data_bound() {
let d = suppress_empty_pages_only_when_real_data_bound(7, true);
assert!(d.run_suppression);
assert_eq!(
d.rule,
AdobeCompatRuleId::SuppressEmptyPagesOnlyWhenRealDataBound
);
}
#[test]
fn suppress_preflight_blocks_when_no_data_bound() {
let d = suppress_empty_pages_only_when_real_data_bound(7, false);
assert!(!d.run_suppression);
}
#[test]
fn suppress_preflight_blocks_when_single_page() {
let d = suppress_empty_pages_only_when_real_data_bound(1, true);
assert!(!d.run_suppression);
}
#[test]
fn suppress_preflight_trace_fires_on_both_branches() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = suppress_empty_pages_only_when_real_data_bound(7, true);
let _ = suppress_empty_pages_only_when_real_data_bound(7, false);
});
let events = sink.events();
assert_eq!(events.len(), 2);
assert_eq!(
events[0].reason.tag(),
"suppress_gated_by_data_bound_signal"
);
assert_eq!(
events[1].reason.tag(),
"suppress_skipped_no_data_bound_signal"
);
}
#[test]
fn bind_none_classifies_regular_field_as_data() {
let c = exclude_bind_none_fields_from_page_data_suppression(true, false, false);
assert_eq!(c, BindNoneClassification::DataField);
}
#[test]
fn bind_none_classifies_bind_none_field_as_excluded() {
let c = exclude_bind_none_fields_from_page_data_suppression(true, false, true);
assert_eq!(c, BindNoneClassification::ExcludedBindNone);
}
#[test]
fn bind_none_classifies_non_data_widget_first() {
let c = exclude_bind_none_fields_from_page_data_suppression(true, true, true);
assert_eq!(c, BindNoneClassification::ExcludedNonDataWidget);
}
#[test]
fn bind_none_summary_silent_when_count_zero() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
emit_bind_none_summary(0);
});
assert!(
sink.events().is_empty(),
"summary must be silent when no field was excluded"
);
}
#[test]
fn bind_none_summary_fires_when_count_positive() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
emit_bind_none_summary(3);
});
let events = sink.events();
assert_eq!(events.len(), 1);
assert_eq!(events[0].phase.tag(), "suppress");
assert_eq!(
events[0].reason.tag(),
"bind_none_field_excluded_from_data_check"
);
let decision = events[0].decision.as_deref().unwrap_or("");
assert!(
decision.contains("excluded_count=3"),
"summary must report exclusion count: {decision}"
);
}
#[test]
fn static_trim_short_circuits_when_not_static() {
let d = static_xfaf_excess_page_trim_with_form_dom_guard(
false, false,
5, None,
);
assert!(!d.allow_trim);
assert!(!d.single_page_collapse);
}
#[test]
fn static_trim_blocked_when_no_form_dom_present() {
for n_layout in [1_usize, 2, 3, 5, 11] {
let d_static =
static_xfaf_excess_page_trim_with_form_dom_guard(true, false, n_layout, None);
assert!(
!d_static.allow_trim,
"n_layout={n_layout} (static, no form DOM): expected no trim, got allow_trim=true"
);
let d_dynamic =
static_xfaf_excess_page_trim_with_form_dom_guard(true, true, n_layout, None);
assert!(
!d_dynamic.allow_trim,
"n_layout={n_layout} (dynamic-logic substring, no form DOM): expected no trim"
);
}
}
#[test]
fn static_trim_allows_when_form_dom_matches_or_under() {
let d = static_xfaf_excess_page_trim_with_form_dom_guard(true, false, 3, Some(2));
assert!(d.allow_trim);
}
#[test]
fn static_trim_blocked_when_form_dom_demands_more_pages() {
let d = static_xfaf_excess_page_trim_with_form_dom_guard(true, false, 7, Some(17));
assert!(!d.allow_trim);
}
#[test]
fn static_trim_single_page_collapse_relaxation() {
let d = static_xfaf_excess_page_trim_with_form_dom_guard(true, false, 1, Some(2));
assert!(d.allow_trim);
assert!(d.single_page_collapse);
}
#[test]
fn static_trim_single_page_collapse_blocked_by_dynamic_logic() {
let d = static_xfaf_excess_page_trim_with_form_dom_guard(true, true, 1, Some(2));
assert!(!d.allow_trim);
}
#[test]
fn static_trim_trace_anchor_fires_on_allow_and_block() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = static_xfaf_excess_page_trim_with_form_dom_guard(true, false, 7, Some(2));
let _ = static_xfaf_excess_page_trim_with_form_dom_guard(true, false, 7, Some(17));
});
let events = sink.events();
assert_eq!(events.len(), 2);
assert_eq!(events[0].reason.tag(), "static_xfaf_trim_allowed");
assert_eq!(events[1].reason.tag(), "static_xfaf_trim_blocked");
}
#[test]
fn static_trim_silent_when_rule_inapplicable() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = static_xfaf_excess_page_trim_with_form_dom_guard(false, false, 3, Some(7));
});
assert!(sink.events().is_empty());
}
#[test]
fn invisible_binding_accepts_visible() {
assert_eq!(
ignore_invisible_server_metadata_bindings(""),
InvisibleBindingDecision::AcceptedVisible
);
assert_eq!(
ignore_invisible_server_metadata_bindings("visible"),
InvisibleBindingDecision::AcceptedVisible
);
assert_eq!(
ignore_invisible_server_metadata_bindings("future-value"),
InvisibleBindingDecision::AcceptedVisible
);
}
#[test]
fn invisible_binding_ignores_invisible_hidden_inactive() {
for presence in ["invisible", "hidden", "inactive"] {
assert_eq!(
ignore_invisible_server_metadata_bindings(presence),
InvisibleBindingDecision::IgnoredInvisible,
"presence={presence} must be IgnoredInvisible"
);
}
}
#[test]
fn invisible_binding_rule_per_call_silent() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = ignore_invisible_server_metadata_bindings("invisible");
let _ = ignore_invisible_server_metadata_bindings("visible");
});
assert!(
sink.events().is_empty(),
"per-field decisions must not emit; summary helper is the trace anchor"
);
}
#[test]
fn invisible_binding_summary_silent_on_zero() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
emit_invisible_binding_summary(0);
});
assert!(sink.events().is_empty());
}
#[test]
fn invisible_binding_summary_fires_on_positive() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
emit_invisible_binding_summary(4);
});
let events = sink.events();
assert_eq!(events.len(), 1);
assert_eq!(events[0].phase.tag(), "bind");
assert_eq!(events[0].reason.tag(), "invisible_field_binding_ignored");
assert!(events[0]
.decision
.as_deref()
.unwrap_or("")
.contains("ignored_count=4"));
}
#[test]
fn non_data_widget_classifies_data_field() {
let c = exclude_non_data_widgets_from_page_suppression(false);
assert_eq!(c, WidgetClassification::DataField);
}
#[test]
fn non_data_widget_classifies_widget() {
let c = exclude_non_data_widgets_from_page_suppression(true);
assert_eq!(c, WidgetClassification::ExcludedNonDataWidget);
}
#[test]
fn non_data_widget_per_call_silent() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = exclude_non_data_widgets_from_page_suppression(true);
let _ = exclude_non_data_widgets_from_page_suppression(false);
});
assert!(sink.events().is_empty());
}
#[test]
fn non_data_widget_summary_silent_on_zero() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
emit_non_data_widget_summary(0);
});
assert!(sink.events().is_empty());
}
#[test]
fn non_data_widget_summary_fires_on_positive() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
emit_non_data_widget_summary(2);
});
let events = sink.events();
assert_eq!(events.len(), 1);
assert_eq!(events[0].phase.tag(), "suppress");
assert_eq!(
events[0].reason.tag(),
"non_data_widget_excluded_from_data_check"
);
assert!(events[0]
.decision
.as_deref()
.unwrap_or("")
.contains("excluded_count=2"));
}
#[test]
fn occur_clamp_passthrough_when_in_range_with_bounded_max() {
let (count, outcome) = repeating_subform_instance_count_clamped_to_occur_range(
"form1.Orders.Order",
2,
0,
Some(5),
);
assert_eq!(count, 2);
assert_eq!(outcome, OccurClampOutcome::PassthroughInRange);
}
#[test]
fn occur_clamp_passthrough_when_unbounded() {
let (count, outcome) =
repeating_subform_instance_count_clamped_to_occur_range("Orders.Order", 5, 0, None);
assert_eq!(count, 5);
assert_eq!(outcome, OccurClampOutcome::PassthroughInRange);
}
#[test]
fn occur_clamp_caps_when_data_exceeds_max() {
let (count, outcome) =
repeating_subform_instance_count_clamped_to_occur_range("Orders.Order", 5, 0, Some(2));
assert_eq!(count, 2);
assert_eq!(outcome, OccurClampOutcome::ClampedByMax);
}
#[test]
fn occur_clamp_lifts_when_data_below_min() {
let (count, outcome) =
repeating_subform_instance_count_clamped_to_occur_range("Orders.Order", 1, 3, None);
assert_eq!(count, 3);
assert_eq!(outcome, OccurClampOutcome::LiftedByMin);
}
#[test]
fn occur_clamp_lifts_when_data_zero_and_min_one() {
let (count, outcome) =
repeating_subform_instance_count_clamped_to_occur_range("Orders.Order", 0, 1, None);
assert_eq!(count, 1);
assert_eq!(outcome, OccurClampOutcome::LiftedByMin);
}
#[test]
fn occur_clamp_max_below_min_still_honours_min() {
let (count, outcome) =
repeating_subform_instance_count_clamped_to_occur_range("Orders.Order", 1, 1, Some(0));
assert_eq!(count, 1);
assert_eq!(outcome, OccurClampOutcome::PassthroughInRange);
}
#[test]
fn occur_clamp_trace_anchor_fires_clamped_by_max() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = repeating_subform_instance_count_clamped_to_occur_range(
"Orders.Order",
5,
0,
Some(2),
);
});
let events = sink.events();
assert_eq!(events.len(), 1);
assert_eq!(events[0].phase.tag(), "occur");
assert_eq!(events[0].reason.tag(), "data_count_clamped_by_occur_max");
assert_eq!(events[0].som.as_deref(), Some("Orders.Order"));
assert!(events[0].input.as_deref().unwrap_or("").contains("count=2"));
}
#[test]
fn occur_clamp_trace_anchor_fires_lifted_by_min() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ =
repeating_subform_instance_count_clamped_to_occur_range("Orders.Order", 1, 3, None);
});
let events = sink.events();
assert_eq!(events.len(), 1);
assert_eq!(events[0].reason.tag(), "data_count_lifted_by_occur_min");
assert!(events[0].input.as_deref().unwrap_or("").contains("count=3"));
}
#[test]
fn occur_clamp_trace_silent_on_passthrough() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = repeating_subform_instance_count_clamped_to_occur_range(
"Orders.Order",
2,
0,
Some(5),
);
let _ =
repeating_subform_instance_count_clamped_to_occur_range("Orders.Order", 3, 0, None);
});
assert!(
sink.events().is_empty(),
"rule must be silent when data count is in range and not clamped"
);
}
#[test]
fn bind_none_allows_when_bind_not_none() {
let g = bind_none_subform_does_not_auto_expand("Orders.Order", true, false, false);
assert_eq!(g, BindNoneExpansionGate::Allowed);
}
#[test]
fn bind_none_blocks_when_bind_none() {
let g = bind_none_subform_does_not_auto_expand("Orders.Order", true, false, true);
assert_eq!(g, BindNoneExpansionGate::Blocked);
}
#[test]
fn bind_none_blocks_even_when_unnamed() {
let g = bind_none_subform_does_not_auto_expand("Orders.Order", true, true, true);
assert_eq!(g, BindNoneExpansionGate::Blocked);
}
#[test]
fn bind_none_trace_fires_only_when_expansion_would_have_run() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = bind_none_subform_does_not_auto_expand("Orders.Order", true, false, true);
});
let events = sink.events();
assert_eq!(events.len(), 1);
assert_eq!(events[0].phase.tag(), "occur");
assert_eq!(
events[0].reason.tag(),
"bind_none_subform_expansion_skipped"
);
assert_eq!(events[0].som.as_deref(), Some("Orders.Order"));
}
#[test]
fn bind_none_trace_silent_when_bind_not_none() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = bind_none_subform_does_not_auto_expand("Orders.Order", true, false, false);
});
assert!(sink.events().is_empty());
}
#[test]
fn bind_none_trace_silent_when_occur_not_repeating() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = bind_none_subform_does_not_auto_expand("Orders.Order", false, false, true);
});
assert!(sink.events().is_empty());
}
#[test]
fn bind_none_trace_silent_when_name_empty() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = bind_none_subform_does_not_auto_expand("", true, true, true);
});
assert!(sink.events().is_empty());
}
#[test]
fn form_dom_replication_clones_difference_when_form_dom_higher() {
let d = form_dom_driven_repeat_instance_replication("Orders.Order", 5, 2);
assert_eq!(d.clones_to_add, 3);
}
#[test]
fn form_dom_replication_zero_when_equal() {
let d = form_dom_driven_repeat_instance_replication("Orders.Order", 3, 3);
assert_eq!(d.clones_to_add, 0);
}
#[test]
fn form_dom_replication_zero_when_form_dom_smaller() {
let d = form_dom_driven_repeat_instance_replication("Orders.Order", 1, 4);
assert_eq!(d.clones_to_add, 0);
}
#[test]
fn form_dom_replication_zero_when_template_default_is_zero() {
let d = form_dom_driven_repeat_instance_replication("Orders.Order", 5, 0);
assert_eq!(d.clones_to_add, 0);
}
#[test]
fn form_dom_replication_trace_fires_on_positive_clones() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = form_dom_driven_repeat_instance_replication("Orders.Order", 5, 2);
});
let events = sink.events();
assert_eq!(events.len(), 1);
assert_eq!(events[0].phase.tag(), "occur");
assert_eq!(events[0].reason.tag(), "subform_materialised_from_data");
assert_eq!(events[0].som.as_deref(), Some("Orders.Order"));
assert!(events[0].input.as_deref().unwrap_or("").contains("count=5"));
}
#[test]
fn form_dom_replication_trace_silent_on_zero_clones() {
let sink: Rc<RecordingSink> = Rc::new(RecordingSink::new());
with_sink(sink.clone(), || {
let _ = form_dom_driven_repeat_instance_replication("Orders.Order", 3, 3);
let _ = form_dom_driven_repeat_instance_replication("Orders.Order", 1, 4);
let _ = form_dom_driven_repeat_instance_replication("Orders.Order", 5, 0);
});
assert!(
sink.events().is_empty(),
"rule must stay silent when no clones are required"
);
}
}