max / quasi
| 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 three places -- `class_attr`, |
| 23 | //! which writes the attribute, `class_into`, which writes one prefixed name |
| 24 | //! into a buffer, and `makeover_webview::class`, which returns one as a value |
| 25 | //! -- so the literals handed to those three are the whole surface. This test |
| 26 | //! reads them out of the source rather than out of rendered HTML: rendering |
| 27 | //! covers what the test author remembered to describe, and the failure being |
| 28 | //! guarded against is a name nobody thought about. |
| 29 | //! |
| 30 | //! `class_into` is read and `makeover_webview::push_class` is not, which is why |
| 31 | //! this crate calls the former. A name spelled at a `push_class` call would be |
| 32 | //! skipped silently: the reader drops `class(` preceded by an identifier |
| 33 | //! character, because `option_class(`, `part_class(` and `cell_part_class(` all |
| 34 | //! end that way and are makeover answering rather than a literal. |
| 35 | //! |
| 36 | //! What it does not read: a name returned by a helper rather than handed to one |
| 37 | //! of the three. `Webview::arrangement_class` and `Webview::measure_class` each |
| 38 | //! match a description value to a `&'static str`, and those names -- the |
| 39 | //! `list-detail` and `measure-wide` sets -- reach the output through a |
| 40 | //! `class_into` call whose argument is the helper. Both sets are this |
| 41 | //! renderer's, neither has ever been checked here, and closing that is its own |
| 42 | //! change rather than a line in RENDERER_OWN. |
| 43 | //! |
| 44 | //! A literal that is neither makeover's nor declared below fails. Adding one to |
| 45 | //! [`RENDERER_OWN`] is the deliberate act the 0.27.0 defect skipped. |
| 46 | |
| 47 | use BTreeSet; |
| 48 | |
| 49 | /// Classes this renderer owns, with makeover answering for none of them. |
| 50 | /// |
| 51 | /// Two kinds, and both are legitimately not makeover's: |
| 52 | /// |
| 53 | /// - **Behavioural hooks.** `row-select`, `row-activate`, `table-sort`, |
| 54 | /// `chip-remove` and `field-writes` are what the transport binds to. They name |
| 55 | /// what an element *does* on this host, which is a webview concern rather than |
| 56 | /// a description one, and makeover deliberately names no behaviour. |
| 57 | /// - **Structure this renderer assembles.** `region`, `selector`, `form`, |
| 58 | /// `rest` and the rest name containers makeover's vocabulary has no word for |
| 59 | /// because nothing else needs one: makeover styles the things inside them. |
| 60 | /// |
| 61 | /// Anything added here should be one of those two. A name that describes how a |
| 62 | /// thing *looks* belongs in makeover, and putting it here is how the drift this |
| 63 | /// test exists to catch would come back wearing a licence. |
| 64 | const RENDERER_OWN: & = & |
| 65 | "act-submit", |
| 66 | // The popover container an `Outcome::Anchored` lands in, beside every |
| 67 | // region, beside a named control and beside a screen's selection. |
| 68 | // `ae8e8836`. Structure this renderer assembles: the menu inside it is a |
| 69 | // screen and carries makeover's own names, and where the container sits |
| 70 | // relative to what it is anchored to is the app's stylesheet. |
| 71 | "anchored", |
| 72 | // The disclosure a control that asks for a value first is drawn in, its |
| 73 | // summary and its body. `Act::asks` is a description fact and the `details` |
| 74 | // is this renderer's answer to it, so makeover has no word for the wrapper; |
| 75 | // the control and the boxes inside it carry makeover's own names. |
| 76 | "ask", |
| 77 | "ask-body", |
| 78 | "ask-open", |
| 79 | "chip-remove", |
| 80 | // The element a `Node::Code` is drawn in, `<pre>` or `<code>`. `19d7602d`. |
| 81 | // Structure this renderer assembles: makeover has no word for a code block |
| 82 | // because the vocabulary carries the runs and not the container, and what a |
| 83 | // monospace block looks like is the host stylesheet's answer. The runs |
| 84 | // inside it carry `lex-<class>`, which is `layout::Syntax::name` rather than |
| 85 | // a name invented here, and is why those are not listed one by one. |
| 86 | "code", |
| 87 | // A readout the browser keeps writing. The class is a hook rather than a |
| 88 | // look: `clock.js` finds these by `data-clock`, and what a time reads like |
| 89 | // is the prose around it, which makeover already answers for. |
| 90 | "clock", |
| 91 | "field-consults", |
| 92 | // A question answered N times, its slots, one slot, and the two controls |
| 93 | // the reader adds and removes them with. `60d1753c`. A group of controls |
| 94 | // answering one question is a `fieldset` and makeover has no word for one: |
| 95 | // the fields inside carry makeover's own names, and these five are the |
| 96 | // grouping and the hooks `repeat.js` finds a group by. |
| 97 | "field-repeat", |
| 98 | "field-repeat-add", |
| 99 | "field-repeat-blank", |
| 100 | "field-repeat-legend", |
| 101 | "field-repeat-remove", |
| 102 | "field-repeat-slot", |
| 103 | "field-repeat-slots", |
| 104 | // The box one conditional question is drawn in, carrying the three |
| 105 | // `data-reveal` marks. `8fdb814c`. A hook rather than a look: `reveal.js` |
| 106 | // finds it by the attributes and makeover has no word for a wrapper whose |
| 107 | // whole job is to be hidden. |
| 108 | "field-reveal", |
| 109 | // The wrapper that asks a field's suggestion route. A hook like the two |
| 110 | // beside it: the list itself and its entries are `form-suggestions` and |
| 111 | // `form-suggestion`, which are makeover's and are ruled there. |
| 112 | "field-suggests", |
| 113 | "field-writes", |
| 114 | "figure-act", |
| 115 | // `figures` was here until makeover-webview 0.59.0, which names it: the |
| 116 | // strip is `figures_html`'s and this renderer emits it to interleave the |
| 117 | // acts, which is emitting rather than owning. |
| 118 | "form", |
| 119 | "heading", |
| 120 | "notices", |
| 121 | "region", |
| 122 | // The wrapper a region's own question hangs on. `cb62a9dc`, and a |
| 123 | // behavioural hook of the first kind: htmx takes one verb per element, so |
| 124 | // the recompute needs an element of its own, and what it names is what the |
| 125 | // transport binds to rather than anything a reader sees. |
| 126 | "region-consults", |
| 127 | "rest", |
| 128 | "rest-next", |
| 129 | // The numbered strip a description that offered jumps gets in place of the |
| 130 | // position readout. `0ce21f4b`. makeover names no pager at all, so the |
| 131 | // whole family is this renderer's. |
| 132 | "rest-page", |
| 133 | "rest-page-here", |
| 134 | "rest-pages", |
| 135 | "rest-position", |
| 136 | "rest-previous", |
| 137 | "rich", |
| 138 | "row-activate", |
| 139 | // `row-disclose` was here until 2026-09-01, on the reasoning that makeover |
| 140 | // has no word for a branch's chevron. It has had one since makeover-webview |
| 141 | // 0.70.1 (`7942bd7`, "give a nested row a rule"), which put it in |
| 142 | // `NESTING_CLASSES` beside `row-nested` and `row-branch`. This crate's lock |
| 143 | // held makeover-webview at 0.70.0, so the overlap sat here undetected until |
| 144 | // the 0.42.0 cascade forced a resolve. The emitted name was already correct |
| 145 | // throughout; only the claim of ownership was wrong. |
| 146 | "row-menu", |
| 147 | "row-select", |
| 148 | "selector", |
| 149 | // The gutter a table row's tick sits in, and the cell above it in the |
| 150 | // head. A tick takes no column -- it does not narrow, sort or carry data -- |
| 151 | // so makeover's `col-` naming has nothing to say about it, and both cells |
| 152 | // also carry makeover's own `cell` and `table-heading` for their treatment. |
| 153 | // The gutter a table row's chevron sits in, and the cell above it in the |
| 154 | // head. `table-select`'s reasoning, one gutter along. |
| 155 | "table-disclose", |
| 156 | "table-disclose-head", |
| 157 | "table-select", |
| 158 | "table-select-head", |
| 159 | // A table row's held-back acts. `row-menu`'s counterpart, named separately |
| 160 | // for the reason `table-row-current` is: this one is positioned against a |
| 161 | // grid rather than against a line. The `data-menu="row"` attribute beside it |
| 162 | // is deliberately the list row's own, so a host binds one gesture for both. |
| 163 | "table-row-menu", |
| 164 | "table-sort", |
| 165 | "text", |
| 166 | ]; |
| 167 | |
| 168 | /// The source files that can name a class. |
| 169 | const SOURCES: & = & |
| 170 | , |
| 171 | , |
| 172 | , |
| 173 | ]; |
| 174 | |
| 175 | |
| 176 | |
| 177 | let opts = default; |
| 178 | let makeover = names; |
| 179 | let mut stray: = Vecnew; |
| 180 | |
| 181 | for in SOURCES |
| 182 | for in class_literals |
| 183 | if makeover.contains || RENDERER_OWN.contains |
| 184 | continue; |
| 185 | |
| 186 | stray.push; |
| 187 | |
| 188 | |
| 189 | |
| 190 | assert! |
| 191 | stray.is_empty, |
| 192 | "{} class name(s) are neither makeover's nor declared in RENDERER_OWN:\n{}\n\n\ |
| 193 | If makeover already answers for this thing, call its naming function \ |
| 194 | (`class`, `option_class`, `part_class`, `cell_part_class`) instead of \ |
| 195 | spelling the name here -- that is the 0.27.0 defect, where `tabs` and \ |
| 196 | `segmented` rendered flat because makeover's rules say `tab` and \ |
| 197 | `segment`. If it is genuinely this renderer's, a behavioural hook or a \ |
| 198 | container makeover has no word for, add it to RENDERER_OWN and say which.", |
| 199 | stray.len, |
| 200 | stray.join |
| 201 | ; |
| 202 | |
| 203 | |
| 204 | |
| 205 | |
| 206 | // The other direction. A name in both lists means two crates believe they |
| 207 | // own the same class, and the app gets whichever rule wins the cascade. |
| 208 | let opts = default; |
| 209 | let makeover = names; |
| 210 | let overlap: = RENDERER_OWN |
| 211 | .iter |
| 212 | .filter |
| 213 | .collect; |
| 214 | assert! |
| 215 | overlap.is_empty, |
| 216 | "makeover defines {overlap:?}, so this renderer must not claim to own it. \ |
| 217 | Delete the entry from RENDERER_OWN; the emitted name is already correct." |
| 218 | ; |
| 219 | |
| 220 | |
| 221 | |
| 222 | |
| 223 | // A declared exception that no longer corresponds to anything is a licence |
| 224 | // nobody is using, and the next stray name lands next to it and reads as |
| 225 | // company. |
| 226 | let emitted: = SOURCES |
| 227 | .iter |
| 228 | .flat_map |
| 229 | .collect; |
| 230 | let dead: = RENDERER_OWN |
| 231 | .iter |
| 232 | .filter |
| 233 | .collect; |
| 234 | assert! |
| 235 | dead.is_empty, |
| 236 | "RENDERER_OWN declares {dead:?}, which nothing emits any more. Delete them." |
| 237 | ; |
| 238 | |
| 239 | |
| 240 | |
| 241 | |
| 242 | let src = r#" |
| 243 | // class_attr(&["not-a-real-one"]) in a comment |
| 244 | class_attr(&["alpha"], opts, out); |
| 245 | class_attr(&["beta", "gamma"], opts, out); |
| 246 | out.push_str(&escape(&class("delta", opts))); |
| 247 | class_into("epsilon", opts, out); |
| 248 | class_attr(&[part_class(layout::RowPart::Primary)], opts, out); |
| 249 | class_into(option_class(kind), opts, out); |
| 250 | "#; |
| 251 | let found: = class_literals.into_iter.map.collect; |
| 252 | let expected: = |
| 253 | .into_iter |
| 254 | .map |
| 255 | .collect; |
| 256 | // `part_class(...)` and `option_class(...)` are makeover answering, not |
| 257 | // literals, so neither is here. |
| 258 | assert_eq!; |
| 259 | |
| 260 | |
| 261 | /// `(line, class name)` for every literal handed to `class_attr` or `class`. |
| 262 | /// |
| 263 | /// A call whose argument is a naming function rather than a literal contributes |
| 264 | /// nothing, which is the point: that call is makeover answering, and this test |
| 265 | /// is only interested in the names this crate spells itself. |
| 266 | |
| 267 | let mut out = Vecnew; |
| 268 | for in src.lines.enumerate |
| 269 | let code = line.trim_start; |
| 270 | // A comment can hold an example, and an example is not an emission. |
| 271 | if code.starts_with |
| 272 | continue; |
| 273 | |
| 274 | // `class_into(` before `class(`, and the two cannot both match: there |
| 275 | // is no `class(` inside `class_into(`. |
| 276 | for in |
| 277 | , |
| 278 | , |
| 279 | , |
| 280 | ] |
| 281 | let mut at = 0; |
| 282 | while let Some = line.find |
| 283 | let start = at + found + call.len; |
| 284 | // `option_class(`, `part_class(` and `cell_part_class(` all end |
| 285 | // in `class(` and are makeover answering rather than a literal. |
| 286 | let is_suffix = call == "class(" |
| 287 | && line |
| 288 | .chars |
| 289 | .next_back |
| 290 | .is_some_and; |
| 291 | at = start; |
| 292 | if is_suffix |
| 293 | continue; |
| 294 | |
| 295 | let Some = line.find else |
| 296 | continue; |
| 297 | ; |
| 298 | for literal in string_literals |
| 299 | out.push; |
| 300 | |
| 301 | |
| 302 | |
| 303 | |
| 304 | out |
| 305 | |
| 306 | |
| 307 | /// The contents of every double-quoted literal in a fragment of Rust. |
| 308 | |
| 309 | let mut out = Vecnew; |
| 310 | let mut rest = fragment; |
| 311 | while let Some = rest.find |
| 312 | rest = &rest; |
| 313 | let Some = rest.find else ; |
| 314 | out.push; |
| 315 | rest = &rest; |
| 316 | |
| 317 | out |
| 318 | |
| 319 |