Skip to main content

max / makenotwork

6.8 KB · 221 lines History Blame Raw
1 //! Tests for the parity harness, and for the per-screen conversion switch.
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 makenotwork::config::QuasiScreens;
16 use parity::Parity;
17
18 // --- what it forgives -------------------------------------------------------
19
20 #[test]
21 fn attribute_order_is_not_a_difference() {
22 Parity::strict().assert(
23 "t",
24 r#"<div id="a" class="card" data-x="1"></div>"#,
25 r#"<div data-x="1" id="a" class="card"></div>"#,
26 );
27 }
28
29 #[test]
30 fn class_order_is_not_a_difference() {
31 // A description emits classes in whatever order it composes them; a
32 // template emits them in the order somebody typed.
33 Parity::strict().assert(
34 "t",
35 r#"<div class="card is-selected raised"></div>"#,
36 r#"<div class="raised card is-selected"></div>"#,
37 );
38 }
39
40 #[test]
41 fn whitespace_between_tags_collapses() {
42 Parity::strict().assert(
43 "t",
44 "<ul>\n <li>one</li>\n <li>two</li>\n</ul>",
45 "<ul><li>one</li><li>two</li></ul>",
46 );
47 }
48
49 #[test]
50 fn runs_of_whitespace_inside_text_collapse_to_one_space() {
51 Parity::strict().assert("t", "<p>one two\n\tthree</p>", "<p>one two three</p>");
52 }
53
54 #[test]
55 fn comments_are_not_a_difference() {
56 Parity::strict().assert(
57 "t",
58 "<div><!-- explains the next line --><span>x</span></div>",
59 "<div><span>x</span></div>",
60 );
61 }
62
63 // --- what it still catches --------------------------------------------------
64
65 #[test]
66 #[should_panic(expected = "does not render the same page")]
67 fn a_changed_attribute_value_fails() {
68 Parity::strict().assert("t", r#"<a href="/a">x</a>"#, r#"<a href="/b">x</a>"#);
69 }
70
71 #[test]
72 #[should_panic(expected = "does not render the same page")]
73 fn a_dropped_class_fails() {
74 Parity::strict().assert(
75 "t",
76 r#"<div class="card raised"></div>"#,
77 r#"<div class="card"></div>"#,
78 );
79 }
80
81 #[test]
82 #[should_panic(expected = "does not render the same page")]
83 fn changed_text_fails() {
84 Parity::strict().assert("t", "<p>Saved.</p>", "<p>Saved</p>");
85 }
86
87 #[test]
88 #[should_panic(expected = "does not render the same page")]
89 fn different_nesting_fails() {
90 // The case dropping end tags would have missed: same tags, same text, and
91 // the region closes in the wrong place.
92 Parity::strict().assert(
93 "t",
94 "<div><section><p>x</p></section></div>",
95 "<div><section></section><p>x</p></div>",
96 );
97 }
98
99 #[test]
100 #[should_panic(expected = "does not render the same page")]
101 fn whitespace_inside_pre_is_content_and_still_compares() {
102 Parity::strict().assert("t", "<pre>one two</pre>", "<pre>one two</pre>");
103 }
104
105 #[test]
106 #[should_panic(expected = "does not render the same page")]
107 fn a_missing_doctype_fails() {
108 // Not pedantry: it is the difference between standards and quirks mode.
109 Parity::strict().assert("t", "<!doctype html><html></html>", "<html></html>");
110 }
111
112 // --- the allowlist ----------------------------------------------------------
113
114 #[test]
115 fn a_named_attribute_can_be_forgiven() {
116 // Retiring the private data-action vocabulary is progress, and it is named
117 // per screen rather than ignored everywhere.
118 Parity::strict().ignoring_attr("data-action").assert(
119 "t",
120 r#"<button data-action="toggleCart" data-arg="7">Buy</button>"#,
121 r#"<button data-arg="7">Buy</button>"#,
122 );
123 }
124
125 #[test]
126 #[should_panic(expected = "does not render the same page")]
127 fn forgiving_one_attribute_does_not_forgive_its_neighbours() {
128 Parity::strict().ignoring_attr("data-action").assert(
129 "t",
130 r#"<button data-action="toggleCart" data-arg="7">Buy</button>"#,
131 r#"<button data-action="toggleCart">Buy</button>"#,
132 );
133 }
134
135 #[test]
136 fn a_named_class_can_be_forgiven() {
137 Parity::strict().ignoring_class("raised").assert(
138 "t",
139 r#"<div class="card"></div>"#,
140 r#"<div class="card raised"></div>"#,
141 );
142 }
143
144 #[test]
145 fn a_class_attribute_emptied_by_the_allowlist_matches_having_none() {
146 Parity::strict().ignoring_class("raised").assert(
147 "t",
148 "<div></div>",
149 r#"<div class="raised"></div>"#,
150 );
151 }
152
153 // --- the failure message ----------------------------------------------------
154
155 #[test]
156 fn the_failure_names_the_screen_and_shows_both_sides() {
157 let err = std::panic::catch_unwind(|| {
158 Parity::strict().assert("item_pricing", "<p>a</p>", "<p>b</p>");
159 })
160 .expect_err("it fails");
161 let msg = err
162 .downcast_ref::<String>()
163 .expect("the panic carries a message");
164
165 assert!(msg.contains("item_pricing"), "names the screen: {msg}");
166 assert!(msg.contains("Askama"), "shows the Askama side: {msg}");
167 assert!(msg.contains("quasi"), "shows the quasi side: {msg}");
168 assert!(
169 msg.contains("ignoring_attr"),
170 "says what to do about an intended difference: {msg}"
171 );
172 }
173
174 // --- the per-screen switch --------------------------------------------------
175
176 #[test]
177 fn no_screen_is_converted_by_default() {
178 // The state every deployment is in until Max flips one.
179 let screens = QuasiScreens::default();
180 assert!(!screens.enabled("item_pricing"));
181 assert!(!screens.any());
182 }
183
184 #[test]
185 fn an_unset_variable_converts_nothing() {
186 let screens = QuasiScreens::parse("");
187 assert!(!screens.enabled("item_pricing"));
188 assert!(!screens.any());
189 }
190
191 #[test]
192 fn screens_are_named_one_at_a_time() {
193 let screens = QuasiScreens::parse("user_account,item_pricing");
194 assert!(screens.enabled("user_account"));
195 assert!(screens.enabled("item_pricing"));
196 assert!(!screens.enabled("project_content"));
197 assert!(screens.any());
198 }
199
200 #[test]
201 fn whitespace_and_blanks_in_the_list_are_ignored() {
202 let screens = QuasiScreens::parse(" user_account , , item_pricing ,");
203 assert!(screens.enabled("user_account"));
204 assert!(screens.enabled("item_pricing"));
205 assert!(!screens.enabled(""));
206 }
207
208 #[test]
209 fn a_star_converts_everything_for_local_work() {
210 let screens = QuasiScreens::parse("*");
211 assert!(screens.enabled("anything_at_all"));
212 assert!(screens.any());
213 }
214
215 #[test]
216 fn a_typo_disables_a_screen_rather_than_enabling_another() {
217 // The safe direction, and the reason nothing validates against a registry.
218 let screens = QuasiScreens::parse("item_pricng");
219 assert!(!screens.enabled("item_pricing"));
220 }
221