Skip to main content

max / makenotwork

6.0 KB · 185 lines History Blame Raw
1 //! Tests for the parity harness.
2 //!
3 //! The harness is what every phase-3 conversion trusts, so it is tested on both
4 //! sides: that it ignores what it claims to ignore, and that it still fails on
5 //! everything else. A parity harness that quietly passes is worse than none,
6 //! because the conversion's whole safety argument is that this file said yes.
7
8 // Just the one module, not `mod harness;`. These are unit tests for the
9 // normalizer and the switch: neither serves a request, so pulling the database,
10 // storage and Stripe harnesses in would cost a much slower build for nothing.
11 // The conversions' own parity tests go through the full harness.
12 #[path = "harness/parity.rs"]
13 mod parity;
14
15 use parity::Parity;
16
17 // --- what it forgives -------------------------------------------------------
18
19 #[test]
20 fn attribute_order_is_not_a_difference() {
21 Parity::strict().assert(
22 "t",
23 r#"<div id="a" class="card" data-x="1"></div>"#,
24 r#"<div data-x="1" id="a" class="card"></div>"#,
25 );
26 }
27
28 #[test]
29 fn class_order_is_not_a_difference() {
30 // A description emits classes in whatever order it composes them; a
31 // template emits them in the order somebody typed.
32 Parity::strict().assert(
33 "t",
34 r#"<div class="card is-selected raised"></div>"#,
35 r#"<div class="raised card is-selected"></div>"#,
36 );
37 }
38
39 #[test]
40 fn whitespace_between_tags_collapses() {
41 Parity::strict().assert(
42 "t",
43 "<ul>\n <li>one</li>\n <li>two</li>\n</ul>",
44 "<ul><li>one</li><li>two</li></ul>",
45 );
46 }
47
48 #[test]
49 fn runs_of_whitespace_inside_text_collapse_to_one_space() {
50 Parity::strict().assert("t", "<p>one two\n\tthree</p>", "<p>one two three</p>");
51 }
52
53 #[test]
54 fn comments_are_not_a_difference() {
55 Parity::strict().assert(
56 "t",
57 "<div><!-- explains the next line --><span>x</span></div>",
58 "<div><span>x</span></div>",
59 );
60 }
61
62 // --- what it still catches --------------------------------------------------
63
64 #[test]
65 #[should_panic(expected = "does not render the same page")]
66 fn a_changed_attribute_value_fails() {
67 Parity::strict().assert("t", r#"<a href="/a">x</a>"#, r#"<a href="/b">x</a>"#);
68 }
69
70 #[test]
71 #[should_panic(expected = "does not render the same page")]
72 fn a_dropped_class_fails() {
73 Parity::strict().assert(
74 "t",
75 r#"<div class="card raised"></div>"#,
76 r#"<div class="card"></div>"#,
77 );
78 }
79
80 #[test]
81 #[should_panic(expected = "does not render the same page")]
82 fn changed_text_fails() {
83 Parity::strict().assert("t", "<p>Saved.</p>", "<p>Saved</p>");
84 }
85
86 #[test]
87 #[should_panic(expected = "does not render the same page")]
88 fn different_nesting_fails() {
89 // The case dropping end tags would have missed: same tags, same text, and
90 // the region closes in the wrong place.
91 Parity::strict().assert(
92 "t",
93 "<div><section><p>x</p></section></div>",
94 "<div><section></section><p>x</p></div>",
95 );
96 }
97
98 #[test]
99 #[should_panic(expected = "does not render the same page")]
100 fn whitespace_inside_pre_is_content_and_still_compares() {
101 Parity::strict().assert("t", "<pre>one two</pre>", "<pre>one two</pre>");
102 }
103
104 #[test]
105 #[should_panic(expected = "does not render the same page")]
106 fn a_missing_doctype_fails() {
107 // Not pedantry: it is the difference between standards and quirks mode.
108 Parity::strict().assert("t", "<!doctype html><html></html>", "<html></html>");
109 }
110
111 // --- the allowlist ----------------------------------------------------------
112
113 #[test]
114 fn a_named_attribute_can_be_forgiven() {
115 // Retiring the private data-action vocabulary is progress, and it is named
116 // per screen rather than ignored everywhere.
117 Parity::strict().ignoring_attr("data-action").assert(
118 "t",
119 r#"<button data-action="toggleCart" data-arg="7">Buy</button>"#,
120 r#"<button data-arg="7">Buy</button>"#,
121 );
122 }
123
124 #[test]
125 #[should_panic(expected = "does not render the same page")]
126 fn forgiving_one_attribute_does_not_forgive_its_neighbours() {
127 Parity::strict().ignoring_attr("data-action").assert(
128 "t",
129 r#"<button data-action="toggleCart" data-arg="7">Buy</button>"#,
130 r#"<button data-action="toggleCart">Buy</button>"#,
131 );
132 }
133
134 #[test]
135 fn a_named_class_can_be_forgiven() {
136 Parity::strict().ignoring_class("raised").assert(
137 "t",
138 r#"<div class="card"></div>"#,
139 r#"<div class="card raised"></div>"#,
140 );
141 }
142
143 #[test]
144 fn a_class_attribute_emptied_by_the_allowlist_matches_having_none() {
145 Parity::strict().ignoring_class("raised").assert(
146 "t",
147 "<div></div>",
148 r#"<div class="raised"></div>"#,
149 );
150 }
151
152 // --- the failure message ----------------------------------------------------
153
154 #[test]
155 fn the_failure_names_the_screen_and_shows_both_sides() {
156 let err = std::panic::catch_unwind(|| {
157 Parity::strict().assert("item_pricing", "<p>a</p>", "<p>b</p>");
158 })
159 .expect_err("it fails");
160 let msg = err
161 .downcast_ref::<String>()
162 .expect("the panic carries a message");
163
164 assert!(msg.contains("item_pricing"), "names the screen: {msg}");
165 assert!(msg.contains("Askama"), "shows the Askama side: {msg}");
166 assert!(msg.contains("quasi"), "shows the quasi side: {msg}");
167 assert!(
168 msg.contains("ignoring_attr"),
169 "says what to do about an intended difference: {msg}"
170 );
171 }
172
173 // --- the per-screen switch --------------------------------------------------
174 //
175 // Six tests lived here until 2026-08-26, covering `QuasiScreens::parse`: the
176 // default converting nothing, an unset variable, naming screens one at a time,
177 // whitespace and blanks, `*` for local work, and a typo disabling a screen
178 // rather than enabling another. `64b33b26` deleted the switch, so they have no
179 // subject.
180 //
181 // The normalizer above is untouched and is the half worth keeping: a phase-3
182 // conversion still wants to compare its described rendering against the Askama
183 // one it replaces. What it cannot do any more is serve both from one binary and
184 // diff them over a request, because there is no flag to serve the old one.
185