|
1 |
+ |
//! Every class this renderer emits is either one makeover defines or one this
|
|
2 |
+ |
//! renderer declares as its own, and nothing in between.
|
|
3 |
+ |
//!
|
|
4 |
+ |
//! # Why this test exists
|
|
5 |
+ |
//!
|
|
6 |
+ |
//! makeover-webview 0.27.0 exists because this crate spelled `tabs`, `segmented`
|
|
7 |
+ |
//! and `option` itself while makeover's rules key off `tab`, `segment` and
|
|
8 |
+ |
//! `toggle`. Every described selector rendered flat: no depth, no focus ring, no
|
|
9 |
+ |
//! chosen state. The CSS stayed valid, the markup stayed valid, and the two
|
|
10 |
+ |
//! simply did not meet. Nothing failed, so it shipped, and it was found by
|
|
11 |
+ |
//! reading rather than by building.
|
|
12 |
+ |
//!
|
|
13 |
+ |
//! The same shape produced `tone-info` / `tone-success` / `tone-warning` /
|
|
14 |
+ |
//! `tone-danger` as classes when makeover keys tone off `data-tone`, so every
|
|
15 |
+ |
//! toned thing arrived with a class no stylesheet in the tree had heard of.
|
|
16 |
+ |
//!
|
|
17 |
+ |
//! Both were a name invented in this crate that makeover already answered for.
|
|
18 |
+ |
//! Neither could be caught by a type, because both sides are strings.
|
|
19 |
+ |
//!
|
|
20 |
+ |
//! # How it reads
|
|
21 |
+ |
//!
|
|
22 |
+ |
//! Class names reach the output through exactly two places -- `class_attr`,
|
|
23 |
+ |
//! which writes the attribute, and `makeover_webview::class`, which prefixes one
|
|
24 |
+ |
//! name -- so the literals handed to those two are the whole surface. This test
|
|
25 |
+ |
//! reads them out of the source rather than out of rendered HTML: rendering
|
|
26 |
+ |
//! covers what the test author remembered to describe, and the failure being
|
|
27 |
+ |
//! guarded against is a name nobody thought about.
|
|
28 |
+ |
//!
|
|
29 |
+ |
//! A literal that is neither makeover's nor declared below fails. Adding one to
|
|
30 |
+ |
//! [`RENDERER_OWN`] is the deliberate act the 0.27.0 defect skipped.
|
|
31 |
+ |
|
|
32 |
+ |
use std::collections::BTreeSet;
|
|
33 |
+ |
|
|
34 |
+ |
/// Classes this renderer owns, with makeover answering for none of them.
|
|
35 |
+ |
///
|
|
36 |
+ |
/// Two kinds, and both are legitimately not makeover's:
|
|
37 |
+ |
///
|
|
38 |
+ |
/// - **Behavioural hooks.** `row-select`, `row-activate`, `table-sort`,
|
|
39 |
+ |
/// `chip-remove` and `field-writes` are what the transport binds to. They name
|
|
40 |
+ |
/// what an element *does* on this host, which is a webview concern rather than
|
|
41 |
+ |
/// a description one, and makeover deliberately names no behaviour.
|
|
42 |
+ |
/// - **Structure this renderer assembles.** `region`, `selector`, `figures`,
|
|
43 |
+ |
/// `form`, `rest` and the rest name containers makeover's vocabulary has no
|
|
44 |
+ |
/// word for because nothing else needs one: makeover styles the things inside
|
|
45 |
+ |
/// them.
|
|
46 |
+ |
///
|
|
47 |
+ |
/// Anything added here should be one of those two. A name that describes how a
|
|
48 |
+ |
/// thing *looks* belongs in makeover, and putting it here is how the drift this
|
|
49 |
+ |
/// test exists to catch would come back wearing a licence.
|
|
50 |
+ |
const RENDERER_OWN: &[&str] = &[
|
|
51 |
+ |
"act-submit",
|
|
52 |
+ |
"chip-remove",
|
|
53 |
+ |
"field-writes",
|
|
54 |
+ |
"figure-act",
|
|
55 |
+ |
"figures",
|
|
56 |
+ |
"form",
|
|
57 |
+ |
"heading",
|
|
58 |
+ |
"notices",
|
|
59 |
+ |
"region",
|
|
60 |
+ |
"rest",
|
|
61 |
+ |
"rest-more",
|
|
62 |
+ |
"rich",
|
|
63 |
+ |
"row-activate",
|
|
64 |
+ |
"row-menu",
|
|
65 |
+ |
"row-select",
|
|
66 |
+ |
"selector",
|
|
67 |
+ |
"table-sort",
|
|
68 |
+ |
"text",
|
|
69 |
+ |
];
|
|
70 |
+ |
|
|
71 |
+ |
/// The source files that can name a class.
|
|
72 |
+ |
const SOURCES: &[(&str, &str)] = &[
|
|
73 |
+ |
("src/node.rs", include_str!("../src/node.rs")),
|
|
74 |
+ |
("src/lib.rs", include_str!("../src/lib.rs")),
|
|
75 |
+ |
("src/shell.rs", include_str!("../src/shell.rs")),
|
|
76 |
+ |
];
|
|
77 |
+ |
|
|
78 |
+ |
#[test]
|
|
79 |
+ |
fn every_class_this_renderer_emits_is_makeovers_or_declared_as_its_own() {
|
|
80 |
+ |
let opts = makeover_webview::Emit::default();
|
|
81 |
+ |
let makeover = makeover_webview::vocabulary::names(&opts);
|
|
82 |
+ |
let mut stray: Vec<String> = Vec::new();
|
|
83 |
+ |
|
|
84 |
+ |
for (name, src) in SOURCES {
|
|
85 |
+ |
for (line, literal) in class_literals(src) {
|
|
86 |
+ |
if makeover.contains(&literal) || RENDERER_OWN.contains(&literal.as_str()) {
|
|
87 |
+ |
continue;
|
|
88 |
+ |
}
|
|
89 |
+ |
stray.push(format!(" {name}:{line} \"{literal}\""));
|
|
90 |
+ |
}
|
|
91 |
+ |
}
|
|
92 |
+ |
|
|
93 |
+ |
assert!(
|
|
94 |
+ |
stray.is_empty(),
|
|
95 |
+ |
"{} class name(s) are neither makeover's nor declared in RENDERER_OWN:\n{}\n\n\
|
|
96 |
+ |
If makeover already answers for this thing, call its naming function \
|
|
97 |
+ |
(`class`, `option_class`, `part_class`, `cell_part_class`) instead of \
|
|
98 |
+ |
spelling the name here -- that is the 0.27.0 defect, where `tabs` and \
|
|
99 |
+ |
`segmented` rendered flat because makeover's rules say `tab` and \
|
|
100 |
+ |
`segment`. If it is genuinely this renderer's, a behavioural hook or a \
|
|
101 |
+ |
container makeover has no word for, add it to RENDERER_OWN and say which.",
|
|
102 |
+ |
stray.len(),
|
|
103 |
+ |
stray.join("\n")
|
|
104 |
+ |
);
|
|
105 |
+ |
}
|
|
106 |
+ |
|
|
107 |
+ |
#[test]
|
|
108 |
+ |
fn nothing_this_renderer_claims_as_its_own_is_something_makeover_already_names() {
|
|
109 |
+ |
// The other direction. A name in both lists means two crates believe they
|
|
110 |
+ |
// own the same class, and the app gets whichever rule wins the cascade.
|
|
111 |
+ |
let opts = makeover_webview::Emit::default();
|
|
112 |
+ |
let makeover = makeover_webview::vocabulary::names(&opts);
|
|
113 |
+ |
let overlap: Vec<&&str> = RENDERER_OWN
|
|
114 |
+ |
.iter()
|
|
115 |
+ |
.filter(|name| makeover.contains(**name))
|
|
116 |
+ |
.collect();
|
|
117 |
+ |
assert!(
|
|
118 |
+ |
overlap.is_empty(),
|
|
119 |
+ |
"makeover defines {overlap:?}, so this renderer must not claim to own it. \
|
|
120 |
+ |
Delete the entry from RENDERER_OWN; the emitted name is already correct."
|
|
121 |
+ |
);
|
|
122 |
+ |
}
|
|
123 |
+ |
|
|
124 |
+ |
#[test]
|
|
125 |
+ |
fn renderer_own_carries_nothing_that_stopped_being_emitted() {
|
|
126 |
+ |
// A declared exception that no longer corresponds to anything is a licence
|
|
127 |
+ |
// nobody is using, and the next stray name lands next to it and reads as
|
|
128 |
+ |
// company.
|
|
129 |
+ |
let emitted: BTreeSet<String> = SOURCES
|
|
130 |
+ |
.iter()
|
|
131 |
+ |
.flat_map(|(_, src)| class_literals(src).into_iter().map(|(_, l)| l))
|
|
132 |
+ |
.collect();
|
|
133 |
+ |
let dead: Vec<&&str> = RENDERER_OWN
|
|
134 |
+ |
.iter()
|
|
135 |
+ |
.filter(|name| !emitted.contains(**name))
|
|
136 |
+ |
.collect();
|
|
137 |
+ |
assert!(
|
|
138 |
+ |
dead.is_empty(),
|
|
139 |
+ |
"RENDERER_OWN declares {dead:?}, which nothing emits any more. Delete them."
|
|
140 |
+ |
);
|
|
141 |
+ |
}
|
|
142 |
+ |
|
|
143 |
+ |
#[test]
|
|
144 |
+ |
fn the_reader_finds_both_call_shapes_and_ignores_prose() {
|
|
145 |
+ |
let src = r#"
|
|
146 |
+ |
// class_attr(&["not-a-real-one"]) in a comment
|
|
147 |
+ |
class_attr(&["alpha"], opts, out);
|
|
148 |
+ |
class_attr(&["beta", "gamma"], opts, out);
|
|
149 |
+ |
out.push_str(&escape(&class("delta", opts)));
|
|
150 |
+ |
class_attr(&[part_class(layout::RowPart::Primary)], opts, out);
|
|
151 |
+ |
"#;
|
|
152 |
+ |
let found: BTreeSet<String> = class_literals(src).into_iter().map(|(_, l)| l).collect();
|
|
153 |
+ |
let expected: BTreeSet<String> = ["alpha", "beta", "gamma", "delta"]
|
|
154 |
+ |
.into_iter()
|
|
155 |
+ |
.map(String::from)
|
|
156 |
+ |
.collect();
|
|
157 |
+ |
// `part_class(...)` is makeover answering, not a literal, so it is not here.
|
|
158 |
+ |
assert_eq!(found, expected);
|
|
159 |
+ |
}
|
|
160 |
+ |
|
|
161 |
+ |
/// `(line, class name)` for every literal handed to `class_attr` or `class`.
|
|
162 |
+ |
///
|
|
163 |
+ |
/// A call whose argument is a naming function rather than a literal contributes
|
|
164 |
+ |
/// nothing, which is the point: that call is makeover answering, and this test
|
|
165 |
+ |
/// is only interested in the names this crate spells itself.
|
|
166 |
+ |
fn class_literals(src: &str) -> Vec<(usize, String)> {
|
|
167 |
+ |
let mut out = Vec::new();
|
|
168 |
+ |
for (i, line) in src.lines().enumerate() {
|
|
169 |
+ |
let code = line.trim_start();
|
|
170 |
+ |
// A comment can hold an example, and an example is not an emission.
|
|
171 |
+ |
if code.starts_with("//") {
|
|
172 |
+ |
continue;
|
|
173 |
+ |
}
|
|
174 |
+ |
for (call, open) in [("class_attr(&[", ']'), ("class(", ')')] {
|
|
175 |
+ |
let mut at = 0;
|
|
176 |
+ |
while let Some(found) = line[at..].find(call) {
|
|
177 |
+ |
let start = at + found + call.len();
|
|
178 |
+ |
// `option_class(`, `part_class(` and `cell_part_class(` all end
|
|
179 |
+ |
// in `class(` and are makeover answering rather than a literal.
|
|
180 |
+ |
let is_suffix = call == "class("
|
|
181 |
+ |
&& line[..at + found]
|
|
182 |
+ |
.chars()
|
|
183 |
+ |
.next_back()
|
|
184 |
+ |
.is_some_and(|c| c.is_alphanumeric() || c == '_');
|
|
185 |
+ |
at = start;
|
|
186 |
+ |
if is_suffix {
|
|
187 |
+ |
continue;
|
|
188 |
+ |
}
|
|
189 |
+ |
let Some(end) = line[start..].find(open) else {
|
|
190 |
+ |
continue;
|
|
191 |
+ |
};
|
|
192 |
+ |
for literal in string_literals(&line[start..start + end]) {
|
|
193 |
+ |
out.push((i + 1, literal));
|
|
194 |
+ |
}
|
|
195 |
+ |
}
|
|
196 |
+ |
}
|
|
197 |
+ |
}
|
|
198 |
+ |
out
|
|
199 |
+ |
}
|
|
200 |
+ |
|
|
201 |
+ |
/// The contents of every double-quoted literal in a fragment of Rust.
|
|
202 |
+ |
fn string_literals(fragment: &str) -> Vec<String> {
|
|
203 |
+ |
let mut out = Vec::new();
|
|
204 |
+ |
let mut rest = fragment;
|
|
205 |
+ |
while let Some(open) = rest.find('"') {
|
|
206 |
+ |
rest = &rest[open + 1..];
|
|
207 |
+ |
let Some(close) = rest.find('"') else { break };
|
|
208 |
+ |
out.push(rest[..close].to_string());
|
|
209 |
+ |
rest = &rest[close + 1..];
|
|
210 |
+ |
}
|
|
211 |
+ |
out
|
|
212 |
+ |
}
|