1use std::collections::BTreeMap;
8
9use crate::{
10 DirtyTrackingPlanId, ExecutionBoundary, FieldBindingId, FieldId, FieldTrackingId, FormEntity,
11 FormFieldBinding, FormFieldEntity, FormId, FormOwnershipGraph, SemanticId, SerializableValue,
12 SourceProvenance, TouchedTrackingPlanId,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DirtyTransitionCause {
17 CommittedFieldValueWrite,
18 ResetField,
19 ResetForm,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum TouchedTransitionCause {
24 BoundControlBlur,
25 ResetField,
26 ResetForm,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct DirtyTransitionPlan {
32 pub committed_write: DirtyTransitionCause,
33 pub reset_field: DirtyTransitionCause,
34 pub reset_form: DirtyTransitionCause,
35 pub compares_with_initial_value: bool,
36 pub may_handoff_to_direct_validation_dependencies: bool,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct TouchedTransitionPlan {
42 pub blur: Option<TouchedTransitionCause>,
43 pub reset_field: TouchedTransitionCause,
44 pub reset_form: TouchedTransitionCause,
45 pub may_handoff_blur_to_target_validation: bool,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct FieldDirtyTracking {
51 pub id: FieldTrackingId,
52 pub plan: DirtyTrackingPlanId,
53 pub field: FieldId,
54 pub form: FormId,
55 pub component: SemanticId,
56 pub initial_value: SerializableValue,
57 pub initial_dirty: bool,
58 pub declaration_order: usize,
59 pub transitions: DirtyTransitionPlan,
60 pub provenance: SourceProvenance,
61 pub initializer_provenance: SourceProvenance,
62 pub boundary: ExecutionBoundary,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct FieldTouchedTracking {
69 pub id: FieldTrackingId,
70 pub plan: TouchedTrackingPlanId,
71 pub field: FieldId,
72 pub form: FormId,
73 pub component: SemanticId,
74 pub initial_touched: bool,
75 pub declaration_order: usize,
76 pub blur_bindings: Vec<FieldBindingId>,
77 pub transitions: TouchedTransitionPlan,
78 pub provenance: SourceProvenance,
79 pub boundary: ExecutionBoundary,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct DirtyTrackingPlan {
84 pub id: DirtyTrackingPlanId,
85 pub form: FormId,
86 pub component: SemanticId,
87 pub fields: Vec<FieldTrackingId>,
88 pub validation: FormTrackingValidation,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct TouchedTrackingPlan {
93 pub id: TouchedTrackingPlanId,
94 pub form: FormId,
95 pub component: SemanticId,
96 pub fields: Vec<FieldTrackingId>,
97 pub validation: FormTrackingValidation,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct FormTrackingIntegrityDiagnostic {
102 pub code: String,
103 pub kind: FormTrackingIntegrityKind,
104 pub message: String,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
108pub enum FormTrackingIntegrityKind {
109 MissingForm,
110 MissingField,
111 PlanIdentityMismatch,
112 TrackingIdentityMismatch,
113 FieldFormMismatch,
114 ComponentMismatch,
115 InitialValueMismatch,
116 BindingMismatch,
117 DuplicateTrackingRecord,
118 MissingTrackingRecord,
119 MissingProvenance,
120 NonCanonicalOrdering,
121}
122
123impl FormTrackingIntegrityKind {
124 #[must_use]
125 pub const fn code(self) -> &'static str {
126 match self {
127 Self::MissingForm => "PSASM1273",
128 Self::MissingField => "PSASM1274",
129 Self::PlanIdentityMismatch => "PSASM1275",
130 Self::TrackingIdentityMismatch => "PSASM1276",
131 Self::FieldFormMismatch => "PSASM1277",
132 Self::ComponentMismatch => "PSASM1278",
133 Self::InitialValueMismatch => "PSASM1279",
134 Self::BindingMismatch => "PSASM1280",
135 Self::DuplicateTrackingRecord => "PSASM1281",
136 Self::MissingTrackingRecord => "PSASM1282",
137 Self::MissingProvenance => "PSASM1283",
138 Self::NonCanonicalOrdering => "PSASM1284",
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct FormTrackingValidation {
145 pub diagnostics: Vec<FormTrackingIntegrityDiagnostic>,
146 pub is_valid: bool,
147}
148
149impl Default for FormTrackingValidation {
150 fn default() -> Self {
151 Self {
152 diagnostics: Vec::new(),
153 is_valid: true,
154 }
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct DirtyTrackingGraph {
162 pub plans: BTreeMap<DirtyTrackingPlanId, DirtyTrackingPlan>,
163 pub fields: BTreeMap<FieldTrackingId, FieldDirtyTracking>,
164 pub validation: FormTrackingValidation,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct TouchedTrackingGraph {
169 pub plans: BTreeMap<TouchedTrackingPlanId, TouchedTrackingPlan>,
170 pub fields: BTreeMap<FieldTrackingId, FieldTouchedTracking>,
171 pub validation: FormTrackingValidation,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct FormTrackingProducts {
176 pub dirty: DirtyTrackingGraph,
177 pub touched: TouchedTrackingGraph,
178}
179
180impl DirtyTrackingGraph {
181 #[must_use]
182 pub fn plan(&self, form: &FormId) -> Option<&DirtyTrackingPlan> {
183 self.plans.get(&DirtyTrackingPlanId::for_form(form))
184 }
185
186 #[must_use]
187 pub fn tracking(&self, field: &FieldId) -> Option<&FieldDirtyTracking> {
188 self.fields.get(&FieldTrackingId::for_field(field))
189 }
190
191 #[must_use]
194 pub fn dirty_after_committed_write(
195 &self,
196 field: &FieldId,
197 current: &SerializableValue,
198 ) -> Option<bool> {
199 self.tracking(field).map(|tracking| {
200 !structurally_equal_serializable_values(current, &tracking.initial_value)
201 })
202 }
203}
204
205impl TouchedTrackingGraph {
206 #[must_use]
207 pub fn plan(&self, form: &FormId) -> Option<&TouchedTrackingPlan> {
208 self.plans.get(&TouchedTrackingPlanId::for_form(form))
209 }
210
211 #[must_use]
212 pub fn tracking(&self, field: &FieldId) -> Option<&FieldTouchedTracking> {
213 self.fields.get(&FieldTrackingId::for_field(field))
214 }
215
216 #[must_use]
217 pub fn blur_bindings(&self, field: &FieldId) -> Vec<&FieldBindingId> {
218 self.tracking(field)
219 .map_or_else(Vec::new, |tracking| tracking.blur_bindings.iter().collect())
220 }
221}
222
223#[must_use]
225pub fn structurally_equal_serializable_values(
226 left: &SerializableValue,
227 right: &SerializableValue,
228) -> bool {
229 left == right
230}
231
232#[must_use]
233#[allow(clippy::too_many_lines)]
234pub fn collect_form_tracking_products(
239 forms: &BTreeMap<FormId, FormEntity>,
240 fields: &BTreeMap<FieldId, FormFieldEntity>,
241 bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
242 ownership: &FormOwnershipGraph,
243) -> FormTrackingProducts {
244 let mut dirty = DirtyTrackingGraph {
245 plans: BTreeMap::new(),
246 fields: BTreeMap::new(),
247 validation: FormTrackingValidation::default(),
248 };
249 let mut touched = TouchedTrackingGraph {
250 plans: BTreeMap::new(),
251 fields: BTreeMap::new(),
252 validation: FormTrackingValidation::default(),
253 };
254
255 for form in forms.values() {
256 let dirty_id = DirtyTrackingPlanId::for_form(&form.id);
257 let touched_id = TouchedTrackingPlanId::for_form(&form.id);
258 let fields_for_form = ordered_fields(fields, &form.id);
259 let mut tracking_ids = Vec::with_capacity(fields_for_form.len());
260
261 for field in fields_for_form {
262 let tracking_id = FieldTrackingId::for_field(&field.id);
263 tracking_ids.push(tracking_id.clone());
264 let blur_bindings = ordered_bindings_for_field(bindings, &field.id)
265 .into_iter()
266 .filter(|binding| {
267 binding.form == form.id
268 && binding.component == field.owner_component
269 && binding.field == field.id
270 })
271 .map(|binding| binding.id.clone())
272 .collect::<Vec<_>>();
273 dirty.fields.insert(
274 tracking_id.clone(),
275 FieldDirtyTracking {
276 id: tracking_id.clone(),
277 plan: dirty_id.clone(),
278 field: field.id.clone(),
279 form: form.id.clone(),
280 component: field.owner_component.clone(),
281 initial_value: field.initial_value.clone(),
282 initial_dirty: false,
283 declaration_order: field.declaration_order,
284 transitions: DirtyTransitionPlan {
285 committed_write: DirtyTransitionCause::CommittedFieldValueWrite,
286 reset_field: DirtyTransitionCause::ResetField,
287 reset_form: DirtyTransitionCause::ResetForm,
288 compares_with_initial_value: true,
289 may_handoff_to_direct_validation_dependencies: true,
290 },
291 provenance: field.provenance.clone(),
292 initializer_provenance: field.initializer_provenance.clone(),
293 boundary: ExecutionBoundary::Client,
294 },
295 );
296 touched.fields.insert(
297 tracking_id.clone(),
298 FieldTouchedTracking {
299 id: tracking_id,
300 plan: touched_id.clone(),
301 field: field.id.clone(),
302 form: form.id.clone(),
303 component: field.owner_component.clone(),
304 initial_touched: false,
305 declaration_order: field.declaration_order,
306 blur_bindings: blur_bindings.clone(),
307 transitions: TouchedTransitionPlan {
308 blur: (!blur_bindings.is_empty())
309 .then_some(TouchedTransitionCause::BoundControlBlur),
310 reset_field: TouchedTransitionCause::ResetField,
311 reset_form: TouchedTransitionCause::ResetForm,
312 may_handoff_blur_to_target_validation: !blur_bindings.is_empty(),
313 },
314 provenance: field.provenance.clone(),
315 boundary: ExecutionBoundary::Client,
316 },
317 );
318 }
319
320 dirty.plans.insert(
321 dirty_id.clone(),
322 DirtyTrackingPlan {
323 id: dirty_id,
324 form: form.id.clone(),
325 component: form
326 .owner
327 .entity_id()
328 .expect("valid Form has component owner")
329 .clone(),
330 fields: tracking_ids.clone(),
331 validation: FormTrackingValidation::default(),
332 },
333 );
334 touched.plans.insert(
335 touched_id.clone(),
336 TouchedTrackingPlan {
337 id: touched_id,
338 form: form.id.clone(),
339 component: form
340 .owner
341 .entity_id()
342 .expect("valid Form has component owner")
343 .clone(),
344 fields: tracking_ids,
345 validation: FormTrackingValidation::default(),
346 },
347 );
348 }
349
350 dirty.validation = validate_dirty_tracking_graph(&dirty, forms, fields, bindings, ownership);
351 touched.validation =
352 validate_touched_tracking_graph(&touched, forms, fields, bindings, ownership);
353 for plan in dirty.plans.values_mut() {
354 plan.validation = dirty.validation.clone();
355 }
356 for plan in touched.plans.values_mut() {
357 plan.validation = touched.validation.clone();
358 }
359 FormTrackingProducts { dirty, touched }
360}
361
362#[must_use]
363#[allow(clippy::too_many_lines)]
364pub fn validate_dirty_tracking_graph(
369 graph: &DirtyTrackingGraph,
370 forms: &BTreeMap<FormId, FormEntity>,
371 fields: &BTreeMap<FieldId, FormFieldEntity>,
372 _bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
373 ownership: &FormOwnershipGraph,
374) -> FormTrackingValidation {
375 let mut validation = FormTrackingValidation::default();
376 for form in forms.values() {
377 let plan_id = DirtyTrackingPlanId::for_form(&form.id);
378 let Some(plan) = graph.plans.get(&plan_id) else {
379 push(
380 &mut validation,
381 FormTrackingIntegrityKind::MissingForm,
382 format!("Form `{}` has no dirty tracking plan", form.id.as_str()),
383 );
384 continue;
385 };
386 if plan.id != plan_id
387 || plan.form != form.id
388 || plan.component
389 != *form
390 .owner
391 .entity_id()
392 .expect("valid Form has component owner")
393 {
394 push(
395 &mut validation,
396 FormTrackingIntegrityKind::PlanIdentityMismatch,
397 format!(
398 "dirty tracking plan for `{}` has non-canonical identity or owner",
399 form.id.as_str()
400 ),
401 );
402 }
403 if ownership.component_of_form(&form.id) != form.owner.entity_id() {
404 push(
405 &mut validation,
406 FormTrackingIntegrityKind::FieldFormMismatch,
407 format!(
408 "dirty tracking Form `{}` is absent from canonical I5 ownership",
409 form.id.as_str()
410 ),
411 );
412 }
413 let expected = ordered_fields(fields, &form.id)
414 .into_iter()
415 .map(|field| FieldTrackingId::for_field(&field.id))
416 .collect::<Vec<_>>();
417 if plan.fields != expected {
418 push(
419 &mut validation,
420 FormTrackingIntegrityKind::NonCanonicalOrdering,
421 format!(
422 "dirty tracking plan `{}` does not retain I3 field order",
423 plan.id.as_str()
424 ),
425 );
426 }
427 }
428 for field in fields.values() {
429 let id = FieldTrackingId::for_field(&field.id);
430 let Some(record) = graph.fields.get(&id) else {
431 push(
432 &mut validation,
433 FormTrackingIntegrityKind::MissingTrackingRecord,
434 format!("Field `{}` has no dirty tracking record", field.id.as_str()),
435 );
436 continue;
437 };
438 if record.id != id || record.plan != DirtyTrackingPlanId::for_form(&field.owner_form) {
439 push(
440 &mut validation,
441 FormTrackingIntegrityKind::TrackingIdentityMismatch,
442 format!(
443 "dirty tracking for `{}` has non-canonical identity",
444 field.id.as_str()
445 ),
446 );
447 }
448 if record.field != field.id || record.form != field.owner_form {
449 push(
450 &mut validation,
451 FormTrackingIntegrityKind::FieldFormMismatch,
452 format!(
453 "dirty tracking `{}` disagrees with I3 Field ownership",
454 record.id.as_str()
455 ),
456 );
457 }
458 if record.component != field.owner_component {
459 push(
460 &mut validation,
461 FormTrackingIntegrityKind::ComponentMismatch,
462 format!(
463 "dirty tracking `{}` disagrees with I3 Field Component",
464 record.id.as_str()
465 ),
466 );
467 }
468 if ownership.component_of_field(&field.id) != Some(&field.owner_component) {
469 push(
470 &mut validation,
471 FormTrackingIntegrityKind::FieldFormMismatch,
472 format!(
473 "dirty tracking `{}` is absent from canonical I5 Field ownership",
474 record.id.as_str()
475 ),
476 );
477 }
478 if record.initial_value != field.initial_value || record.initial_dirty {
479 push(&mut validation, FormTrackingIntegrityKind::InitialValueMismatch, format!("dirty tracking `{}` does not retain the I3 initial value or clean initial state", record.id.as_str()));
480 }
481 if record.provenance != field.provenance
482 || record.initializer_provenance != field.initializer_provenance
483 {
484 push(
485 &mut validation,
486 FormTrackingIntegrityKind::MissingProvenance,
487 format!(
488 "dirty tracking `{}` lacks canonical field provenance",
489 record.id.as_str()
490 ),
491 );
492 }
493 }
494 for record in graph.fields.values() {
495 if !fields.contains_key(&record.field) {
496 push(
497 &mut validation,
498 FormTrackingIntegrityKind::MissingField,
499 format!(
500 "dirty tracking `{}` references unknown Field",
501 record.id.as_str()
502 ),
503 );
504 }
505 }
506 validation
507}
508
509#[must_use]
510#[allow(clippy::too_many_lines)]
511pub fn validate_touched_tracking_graph(
516 graph: &TouchedTrackingGraph,
517 forms: &BTreeMap<FormId, FormEntity>,
518 fields: &BTreeMap<FieldId, FormFieldEntity>,
519 bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
520 ownership: &FormOwnershipGraph,
521) -> FormTrackingValidation {
522 let mut validation = FormTrackingValidation::default();
523 for form in forms.values() {
524 let plan_id = TouchedTrackingPlanId::for_form(&form.id);
525 let Some(plan) = graph.plans.get(&plan_id) else {
526 push(
527 &mut validation,
528 FormTrackingIntegrityKind::MissingForm,
529 format!("Form `{}` has no touched tracking plan", form.id.as_str()),
530 );
531 continue;
532 };
533 if plan.id != plan_id
534 || plan.form != form.id
535 || plan.component
536 != *form
537 .owner
538 .entity_id()
539 .expect("valid Form has component owner")
540 {
541 push(
542 &mut validation,
543 FormTrackingIntegrityKind::PlanIdentityMismatch,
544 format!(
545 "touched tracking plan for `{}` has non-canonical identity or owner",
546 form.id.as_str()
547 ),
548 );
549 }
550 if ownership.component_of_form(&form.id) != form.owner.entity_id() {
551 push(
552 &mut validation,
553 FormTrackingIntegrityKind::FieldFormMismatch,
554 format!(
555 "touched tracking Form `{}` is absent from canonical I5 ownership",
556 form.id.as_str()
557 ),
558 );
559 }
560 let expected = ordered_fields(fields, &form.id)
561 .into_iter()
562 .map(|field| FieldTrackingId::for_field(&field.id))
563 .collect::<Vec<_>>();
564 if plan.fields != expected {
565 push(
566 &mut validation,
567 FormTrackingIntegrityKind::NonCanonicalOrdering,
568 format!(
569 "touched tracking plan `{}` does not retain I3 field order",
570 plan.id.as_str()
571 ),
572 );
573 }
574 }
575 for field in fields.values() {
576 let id = FieldTrackingId::for_field(&field.id);
577 let Some(record) = graph.fields.get(&id) else {
578 push(
579 &mut validation,
580 FormTrackingIntegrityKind::MissingTrackingRecord,
581 format!(
582 "Field `{}` has no touched tracking record",
583 field.id.as_str()
584 ),
585 );
586 continue;
587 };
588 if record.id != id || record.plan != TouchedTrackingPlanId::for_form(&field.owner_form) {
589 push(
590 &mut validation,
591 FormTrackingIntegrityKind::TrackingIdentityMismatch,
592 format!(
593 "touched tracking for `{}` has non-canonical identity",
594 field.id.as_str()
595 ),
596 );
597 }
598 if record.field != field.id || record.form != field.owner_form {
599 push(
600 &mut validation,
601 FormTrackingIntegrityKind::FieldFormMismatch,
602 format!(
603 "touched tracking `{}` disagrees with I3 Field ownership",
604 record.id.as_str()
605 ),
606 );
607 }
608 if record.component != field.owner_component || record.initial_touched {
609 push(
610 &mut validation,
611 FormTrackingIntegrityKind::ComponentMismatch,
612 format!(
613 "touched tracking `{}` disagrees with I3 owner or clean initial state",
614 record.id.as_str()
615 ),
616 );
617 }
618 if ownership.component_of_field(&field.id) != Some(&field.owner_component) {
619 push(
620 &mut validation,
621 FormTrackingIntegrityKind::FieldFormMismatch,
622 format!(
623 "touched tracking `{}` is absent from canonical I5 Field ownership",
624 record.id.as_str()
625 ),
626 );
627 }
628 if record.provenance != field.provenance {
629 push(
630 &mut validation,
631 FormTrackingIntegrityKind::MissingProvenance,
632 format!(
633 "touched tracking `{}` lacks canonical field provenance",
634 record.id.as_str()
635 ),
636 );
637 }
638 }
639 for tracking in graph.fields.values() {
640 let expected = ordered_bindings_for_field(bindings, &tracking.field)
641 .into_iter()
642 .filter(|binding| {
643 binding.form == tracking.form && binding.component == tracking.component
644 })
645 .map(|binding| binding.id.clone())
646 .collect::<Vec<_>>();
647 if tracking.blur_bindings != expected {
648 push(
649 &mut validation,
650 FormTrackingIntegrityKind::BindingMismatch,
651 format!(
652 "touched tracking `{}` does not retain the canonical bound-control blur set",
653 tracking.id.as_str()
654 ),
655 );
656 }
657 if tracking
658 .blur_bindings
659 .windows(2)
660 .any(|pair| pair[0] >= pair[1])
661 && tracking.blur_bindings.len() > 1
662 {
663 push(
664 &mut validation,
665 FormTrackingIntegrityKind::NonCanonicalOrdering,
666 format!(
667 "touched tracking `{}` blur bindings are not canonical",
668 tracking.id.as_str()
669 ),
670 );
671 }
672 }
673 for record in graph.fields.values() {
674 if !fields.contains_key(&record.field) {
675 push(
676 &mut validation,
677 FormTrackingIntegrityKind::MissingField,
678 format!(
679 "touched tracking `{}` references unknown Field",
680 record.id.as_str()
681 ),
682 );
683 }
684 }
685 validation
686}
687
688fn ordered_fields<'a>(
689 fields: &'a BTreeMap<FieldId, FormFieldEntity>,
690 form: &FormId,
691) -> Vec<&'a FormFieldEntity> {
692 let mut result = fields
693 .values()
694 .filter(|field| &field.owner_form == form)
695 .collect::<Vec<_>>();
696 result.sort_by(|left, right| {
697 (left.declaration_order, &left.id).cmp(&(right.declaration_order, &right.id))
698 });
699 result
700}
701
702fn ordered_bindings_for_field<'a>(
703 bindings: &'a BTreeMap<FieldBindingId, FormFieldBinding>,
704 field: &FieldId,
705) -> Vec<&'a FormFieldBinding> {
706 let mut result = bindings
707 .values()
708 .filter(|binding| &binding.field == field)
709 .collect::<Vec<_>>();
710 result.sort_by(|left, right| {
711 (left.authored_order, &left.id).cmp(&(right.authored_order, &right.id))
712 });
713 result
714}
715
716fn push(validation: &mut FormTrackingValidation, kind: FormTrackingIntegrityKind, message: String) {
717 validation
718 .diagnostics
719 .push(FormTrackingIntegrityDiagnostic {
720 code: kind.code().to_string(),
721 kind,
722 message,
723 });
724 validation.diagnostics.sort_by(|left, right| {
725 (left.code.as_str(), left.message.as_str())
726 .cmp(&(right.code.as_str(), right.message.as_str()))
727 });
728 validation.diagnostics.dedup();
729 validation.is_valid = false;
730}
731
732#[cfg(test)]
733mod tests {
734 use std::collections::BTreeMap;
735
736 use crate::{
737 build_application_semantic_model, DirtyTrackingPlanId, FieldId, FormId, SerializableValue,
738 TouchedTrackingPlanId,
739 };
740
741 fn model() -> crate::ApplicationSemanticModel {
742 let parsed = presolve_parser::parse_file(
743 "src/Profile.tsx",
744 r#"
745@component("profile-editor")
746class ProfileEditor {
747 @form() profile!: Form;
748 @field(this.profile) name = "Austin";
749 @field(this.profile) tags: string[] = ["compiler"];
750 @field(this.profile) contact: "email" | "phone" = "email";
751 @field(this.profile) unbound = false;
752 render() {
753 return <div>
754 <input field={this.name} />
755 <input type="radio" value="email" field={this.contact} />
756 <input type="radio" value="phone" field={this.contact} />
757 </div>;
758 }
759}
760"#,
761 );
762 build_application_semantic_model(&parsed)
763 }
764
765 #[test]
766 fn plans_every_field_and_records_dirty_and_blur_facts() {
767 let model = model();
768 assert!(model.form_tracking.dirty.validation.is_valid);
769 assert!(model.form_tracking.touched.validation.is_valid);
770 let form = FormId::for_owner(&model.components[0].id, "profile");
771 assert!(model
772 .form_tracking
773 .dirty
774 .plans
775 .contains_key(&DirtyTrackingPlanId::for_form(&form)));
776 assert!(model
777 .form_tracking
778 .touched
779 .plans
780 .contains_key(&TouchedTrackingPlanId::for_form(&form)));
781
782 let name = FieldId::for_form(&form, "name");
783 let tags = FieldId::for_form(&form, "tags");
784 let contact = FieldId::for_form(&form, "contact");
785 let unbound = FieldId::for_form(&form, "unbound");
786 assert_eq!(
787 model.form_tracking.dirty.dirty_after_committed_write(
788 &name,
789 &SerializableValue::String("Austin".to_string())
790 ),
791 Some(false)
792 );
793 assert_eq!(
794 model
795 .form_tracking
796 .dirty
797 .dirty_after_committed_write(&name, &SerializableValue::String("Ada".to_string())),
798 Some(true)
799 );
800 assert_eq!(
801 model.form_tracking.dirty.dirty_after_committed_write(
802 &tags,
803 &SerializableValue::Array(vec![SerializableValue::String("compiler".to_string())])
804 ),
805 Some(false)
806 );
807 assert_eq!(model.form_tracking.touched.blur_bindings(&contact).len(), 2);
808 assert!(model
809 .form_tracking
810 .touched
811 .blur_bindings(&unbound)
812 .is_empty());
813 }
814
815 #[test]
816 fn structural_equality_is_recursive_and_key_order_independent() {
817 let mut left = BTreeMap::new();
818 left.insert(
819 "city".to_string(),
820 SerializableValue::String("Austin".to_string()),
821 );
822 left.insert(
823 "zip".to_string(),
824 SerializableValue::String("78701".to_string()),
825 );
826 let mut right = BTreeMap::new();
827 right.insert(
828 "zip".to_string(),
829 SerializableValue::String("78701".to_string()),
830 );
831 right.insert(
832 "city".to_string(),
833 SerializableValue::String("Austin".to_string()),
834 );
835 assert!(super::structurally_equal_serializable_values(
836 &SerializableValue::Object(left),
837 &SerializableValue::Object(right)
838 ));
839 }
840
841 #[test]
842 fn malformed_tracking_is_reported_without_reconstructing_it() {
843 let model = model();
844 let mut dirty = model.form_tracking.dirty.clone();
845 let record = dirty.fields.values_mut().next().expect("dirty record");
846 record.initial_dirty = true;
847 let validation = super::validate_dirty_tracking_graph(
848 &dirty,
849 &model.forms,
850 &model.form_fields,
851 &model.form_field_bindings,
852 &model.form_ownership,
853 );
854 assert!(!validation.is_valid);
855 assert!(validation
856 .diagnostics
857 .iter()
858 .any(|diagnostic| diagnostic.kind
859 == super::FormTrackingIntegrityKind::InitialValueMismatch));
860 }
861
862 #[test]
863 fn remains_internal_and_does_not_change_the_frozen_public_graph() {
864 let model = model();
865 let graph = crate::build_semantic_graph(&model);
866 let json = crate::semantic_graph_json(&graph);
867 assert!(!json.contains("dirty-plan"));
868 assert!(!json.contains("touched-plan"));
869 assert!(!json.contains("/tracking"));
870 }
871}