Skip to main content

max / makeover-webview

54.2 KB · 1301 lines History Blame Raw
1 //! Tests for [`super`].
2
3 use super::*;
4 use makeover_layout::Edge;
5
6 #[test]
7 fn every_fallback_class_is_one_a_checker_knows_about() {
8 // The obligation ROW_PART_CLASSES carries, for the same reason: a class
9 // this crate can write and the vocabulary list does not carry is
10 // invisible to the dead-vocabulary seal and to the overlap check both.
11 for fallback in [
12 Fallback::Wrap,
13 Fallback::Stack,
14 Fallback::Shed,
15 Fallback::Menu,
16 ] {
17 assert!(
18 RUN_CLASSES.contains(&fallback_class(fallback)),
19 "{fallback:?} is missing from RUN_CLASSES"
20 );
21 }
22 let names = crate::vocabulary::names(&Emit::default());
23 for name in RUN_CLASSES {
24 assert!(names.contains(*name), "{name} is not in the vocabulary");
25 }
26 }
27
28 #[test]
29 fn a_run_gives_every_member_a_floor_it_cannot_be_squeezed_below() {
30 // The whole of what stops the overlap, and it is not a fallback: it
31 // applies to every run whatever the group declared. flexbox's default
32 // min-width is auto, which lets an item be compressed below its own
33 // content in a nowrap row, and that is how a toolbar is drawn over a
34 // tab strip even with nothing out of flow.
35 let css = run_rules(&Emit::default());
36 assert!(css.contains(".run > * {\n min-width: min-content;\n}"));
37 // No number anywhere in it. The minimum is derived by the browser from
38 // what the members contain, which is the ruling's own requirement.
39 assert!(!css.contains("px"));
40 assert!(!css.contains("rem"));
41 assert!(!css.contains("@media"));
42 }
43
44 #[test]
45 fn a_member_that_asks_to_fill_absorbs_what_is_left() {
46 // The second half of what a column says, reaching a row of regions
47 // (quasicoherent `cf981aaa`). A zero basis and not `auto`, because equal
48 // division between several fills is `Width::Fill`'s own stated rule and
49 // `auto` divides the leftovers in proportion to the contents instead.
50 let css = run_rules(&Emit::default());
51 assert!(css.contains(".run > [data-width=\"fill\"] {\n flex: 1 1 0;\n}"));
52
53 // The floor is not overridden. A fill that could shrink under its own
54 // contents would overlap its neighbour, which is the rule this whole
55 // mechanism exists to keep.
56 assert!(!css.contains("min-width: 0"));
57
58 // A member that says nothing gets nothing: content is what a flex item
59 // with the floor and no grow already is.
60 assert!(!css.contains("data-width=\"content\""));
61 }
62
63 #[test]
64 fn room_is_never_asked_of_the_viewport() {
65 // The 913 case: a window in SizeClass::Expanded holding a group out of
66 // room. A viewport query answers about the window and would be wrong
67 // about the group, which is why the table's @media walk is not the
68 // precedent this follows.
69 let css = run_rules(&Emit::default());
70 for size in [SizeClass::Compact, SizeClass::Medium] {
71 assert!(!css.contains(&size.media_condition()));
72 }
73 }
74
75 #[test]
76 fn every_fallback_lands_as_a_class_and_an_unknown_one_lands_plainly() {
77 let css = run_rules(&Emit::default());
78 for fallback in [
79 Fallback::Wrap,
80 Fallback::Stack,
81 Fallback::Shed,
82 Fallback::Menu,
83 ] {
84 let class = fallback_class(fallback);
85 assert!(css.contains(&format!(".{class} {{")), "{class} unemitted");
86 }
87 // Stack is the one that also says what a member does with the line it
88 // took, which is what separates it from wrapping.
89 assert!(css.contains(".run-stack > * {\n flex: 1 1 max-content;\n}"));
90 }
91
92 #[test]
93 fn the_emitted_bevel_matches_what_the_apps_already_hand_write() {
94 // Balanced Breakfast's styles.css, verbatim. Adoption has to be a
95 // deletion, not a redesign, or nobody will take it.
96 let opts = Emit::default();
97 assert_eq!(
98 bevel_shadow(Bevel::Raised, &opts),
99 "inset 1px 1px 0 var(--bevel-light), inset -1px -1px 0 var(--bevel-dark)"
100 );
101 assert_eq!(
102 bevel_shadow(Bevel::Inset, &opts),
103 "inset 1px 1px 0 var(--bevel-dark), inset -1px -1px 0 var(--bevel-light)"
104 );
105 }
106
107 #[test]
108 fn no_colour_ever_reaches_the_output() {
109 let css = stylesheet(&Emit::default());
110 assert!(!css.contains('#'), "a hex literal escaped into the CSS");
111 assert!(
112 !css.contains("rgb"),
113 "a colour function escaped into the CSS"
114 );
115 // Every colour is named, never resolved.
116 assert!(css.contains("var(--surface-raised)"));
117 assert!(css.contains("var(--bevel-light)"));
118 }
119
120 #[test]
121 fn a_well_falls_back_through_css_rather_than_through_rust() {
122 assert_eq!(
123 fill_var(Fill::Well),
124 "var(--surface-well, var(--surface-page))"
125 );
126 // Nothing else needs one.
127 assert_eq!(fill_var(Fill::Raised), "var(--surface-raised)");
128 assert_eq!(fill_var(Fill::Page), "var(--surface-page)");
129 }
130
131 #[test]
132 fn raised_and_well_do_not_collapse_onto_each_other() {
133 let css = depth_rules(&Emit::default());
134 assert!(css.contains(".raised {"));
135 assert!(css.contains(".well {"));
136 assert!(css.contains("var(--bevel-raised)"));
137 assert!(css.contains("var(--bevel-inset)"));
138 }
139
140 /// The cast shadow is composed here from the tone `makeover` derives, so
141 /// neither crate has to hold the other's numbers.
142 ///
143 /// It is a `:root` property and deliberately not a depth class. There is no
144 /// `Depth::Overlay` in the description layer, and adding one would be a
145 /// claim about what a screen means rather than about how it is painted;
146 /// until something asks for it, a consumer names the property on the rule
147 /// for the menu or the toast it already has.
148 #[test]
149 fn the_cast_shadow_is_a_root_property_not_a_depth() {
150 let css = bevel_properties(&Emit::default());
151 assert!(css.contains("--elevation-overlay:"));
152 assert!(css.contains("var(--elevation)"));
153 assert!(
154 !depth_rules(&Emit::default()).contains("elevation"),
155 "elevation is not a depth class"
156 );
157 }
158
159 #[test]
160 fn the_cascade_carries_the_pressed_state() {
161 let css = surface_rules(&Emit::default());
162 // The one thing this renderer gets free that the other two resolve by
163 // hand, eighteen call sites deep in audiofiles' case. Asserted on a
164 // named surface: pressing belongs to the control, not to the depth.
165 assert!(css.contains(".card:active {"));
166 assert!(css.contains(".button:active {"));
167 }
168
169 #[test]
170 fn the_depth_class_is_a_surface_and_not_a_control() {
171 let css = depth_rules(&Emit::default());
172 // The static surface the vocabulary was missing. Sixteen goingson
173 // elements wore .card and cancelled its hover and press to get this,
174 // because a raised object that is not pressable had no other spelling.
175 for state in [":hover", ":active", ":focus-visible", ":disabled"] {
176 assert!(
177 !css.contains(&format!(".raised{state}")),
178 "the depth class claimed {state}: {css}"
179 );
180 }
181 assert!(css.contains("var(--bevel-raised)"), "still raised: {css}");
182 }
183
184 #[test]
185 fn pressing_moves_the_fill_and_not_only_the_edge() {
186 // The decision-1 guard, and the regression that mattered: emitting the
187 // bevel flip alone is what left goingson hand-writing `background:
188 // var(--surface-sunken)` on .btn, .card and .tag/.badge alike, so none
189 // of the three could be deleted.
190 let pressed = interactive_rules("button", Depth::Raised, &Emit::default());
191 assert!(pressed.contains(".button:active {"));
192 assert!(
193 pressed.contains("background: var(--surface-well, var(--surface-page))"),
194 "pressed dropped its fill: {pressed}"
195 );
196 assert!(pressed.contains("box-shadow: var(--bevel-inset)"));
197 }
198
199 #[test]
200 fn pressed_takes_its_fill_from_the_description_not_from_the_app() {
201 // goingson presses to --surface-sunken. The description says a pressed
202 // raised region reads as a well, and makeover says outright that
203 // surface-sunken cannot serve as one, so the app is the thing that
204 // moves.
205 //
206 // Scoped to the pressed rules rather than to the whole sheet: since
207 // makeover-layout 0.3.0 an unchosen tab is legitimately
208 // --surface-sunken, so the token appearing somewhere in the output no
209 // longer means the app's choice leaked in.
210 let css = stylesheet(&Emit::default());
211 let mut checked = 0;
212 for rule in css.split("}\n") {
213 if !rule.contains(":active") {
214 continue;
215 }
216 checked += 1;
217 assert!(
218 !rule.contains("surface-sunken"),
219 "a pressed rule took the app's fill: {rule}"
220 );
221 }
222 assert!(checked > 0, "no pressed rules found to check");
223 assert_eq!(
224 Depth::Raised.pressed().fill(),
225 Some(Fill::Well),
226 "the description changed under us"
227 );
228 }
229
230 #[test]
231 fn the_whole_stylesheet_is_emitted_in_the_family_layer() {
232 // The point of 0.11.0. Unlayered normal declarations outrank every
233 // named layer, so an app declaring `@layer base, components` loses
234 // every rule it owns to this file until this file is layered too.
235 let css = stylesheet(&Emit::default());
236 assert!(css.contains(&format!("@layer {CSS_LAYER} {{")));
237
238 // Exactly one layer block, and nothing outside it but the banner.
239 assert_eq!(css.matches("@layer").count(), 2, "banner names it once");
240 let opened = css.find("@layer makeover {").expect("layer opens");
241 for (i, line) in css.lines().enumerate() {
242 let before_layer = css.lines().take(i).map(str::len).sum::<usize>() < opened;
243 if before_layer || line.is_empty() {
244 continue;
245 }
246 assert!(
247 line.starts_with(" ") || line == "}" || line.starts_with(" "),
248 "line outside the layer: {line:?}"
249 );
250 }
251 }
252
253 #[test]
254 fn the_generated_sheet_carries_no_trailing_whitespace() {
255 // A checked-in generated file that a formatter wants to rewrite is a
256 // diff every time somebody saves it.
257 let css = stylesheet(&Emit::default());
258 for (i, line) in css.lines().enumerate() {
259 assert_eq!(line, line.trim_end(), "trailing whitespace on line {i}");
260 }
261 }
262
263 #[test]
264 fn the_banner_tells_an_app_how_to_order_the_layer() {
265 // Without a declared order the layer's position depends on which
266 // generated file the browser sees first, which is not a contract.
267 let css = stylesheet(&Emit::default());
268 assert!(css.contains("@layer makeover, base, components, responsive;"));
269 // And the banner is outside the layer, not a rule inside it.
270 assert!(css.starts_with("/* Generated by makeover-webview"));
271 }
272
273 #[test]
274 fn the_banner_names_the_emitter_so_a_stale_pin_is_visible_on_sight() {
275 // A consumer whose lockfile pins an old version gets a well-formed
276 // sheet with components missing and no error. balanced_breakfast ran
277 // on 657 bytes from a 0.1.0 emitter while its manifest asked for
278 // 0.5.1, and the only way it surfaced was diffing two apps' generated
279 // files. The version and the count are what the file says instead.
280 let css = stylesheet(&Emit::default());
281 let banner = css.lines().next().unwrap();
282 assert!(
283 banner.contains(VERSION),
284 "{banner} does not name the emitter"
285 );
286 let classes = vocabulary::classes_in_css(&css).len();
287 assert!(classes > 0);
288 assert!(
289 banner.contains(&format!("{classes} classes")),
290 "{banner} does not carry the class count"
291 );
292 }
293
294 #[test]
295 fn a_primitive_owns_every_state_it_implies() {
296 // The whole point of 0.10.0. Anything emitting a hover rule owes the
297 // other three, or the consuming app supplies them by out-specifying a
298 // rule it does not own: 19 such rules in goingson, 21 in the MNW
299 // server, and three focus rings that do not match.
300 let css = stylesheet(&Emit::default());
301 for selector in ["button", "card", "chip", "tab", "segment", "toggle"] {
302 assert!(css.contains(&format!(".{selector}:hover {{")), "{selector}");
303 assert!(
304 css.contains(&format!(".{selector}:active {{")),
305 "{selector}"
306 );
307 assert!(
308 css.contains(&format!(".{selector}:focus-visible {{")),
309 "{selector} has no focus ring"
310 );
311 assert!(
312 css.contains(&format!(".{selector}:disabled,")),
313 "{selector} has no disabled state"
314 );
315 }
316 }
317
318 #[test]
319 fn a_field_takes_focus_and_refuses_input_without_taking_a_hover() {
320 // A text field does not light up under the pointer, so it gets the two
321 // states it has and not the two it does not.
322 let css = stylesheet(&Emit::default());
323 assert!(css.contains(".field:focus-visible {"));
324 assert!(css.contains(".field:disabled,"));
325 assert!(!css.contains(".field:hover {"));
326 assert!(!css.contains(".field:active {"));
327 }
328
329 #[test]
330 fn disabled_is_emitted_after_hover_so_source_order_settles_it() {
331 // Every one of these selectors is specificity (0,2,0), so nothing but
332 // order decides which wins. A disabled button taking the hover fill is
333 // the exact bug goingson's `.button:disabled:hover` was written to fix,
334 // and the reason it had to reach (0,3,0) to do it.
335 let css = interactive_rules("button", Depth::Raised, &Emit::default());
336 let hover = css.find(":hover").expect("hover");
337 let active = css.find(":active").expect("active");
338 let focus = css.find(":focus-visible").expect("focus");
339 let disabled = css.find(":disabled").expect("disabled");
340 assert!(hover < active && active < focus && focus < disabled);
341
342 // And it restores the surface, or the hover fill survives underneath.
343 let tail = &css[disabled..];
344 assert!(tail.contains("background: var(--surface-raised)"));
345 }
346
347 #[test]
348 fn a_flat_control_takes_its_hover_fill_back_when_it_stops_answering() {
349 // The same contest one depth over, and the half `depth_declarations`
350 // could not state. Flat declares neither axis, so before 0.68.0 the
351 // disabled rule won on source order with nothing to say and the hover
352 // surface stayed under a control that had stopped answering. Reaches
353 // both facet arms and a suggestion entry.
354 for depth in [Depth::Flat, Depth::Sunken, Depth::Overlay] {
355 let css = disabled_rule("x", depth);
356 assert!(css.contains("box-shadow"), "{depth:?}: {css}");
357 }
358 let flat = disabled_rule("x", Depth::Flat);
359 assert!(flat.contains("background: none;"), "{flat}");
360 assert!(flat.contains("box-shadow: none;"), "{flat}");
361
362 // Every rule the caller emits states both axes now, so whichever wins
363 // the source-order contest leaves nothing of the one below it.
364 let css = interactive_rules("x", Depth::Flat, &Emit::default());
365 let disabled = css.find(":disabled").expect("disabled");
366 assert!(css[disabled..].contains("background: none;"), "{css}");
367 }
368
369 #[test]
370 fn a_disabled_state_reaches_things_that_cannot_be_disabled() {
371 // `:disabled` matches form elements only, and a chip is a div. Keying
372 // on the ARIA attribute too is the pattern the invalid field already
373 // set: one fact, read by the styling and the accessibility tree alike.
374 let css = disabled_rule("chip", Depth::Raised);
375 assert!(css.contains(".chip:disabled,"));
376 assert!(css.contains(".chip[aria-disabled=\"true\"]"));
377 assert!(css.contains("cursor: not-allowed"));
378 }
379
380 #[test]
381 fn the_focus_ring_does_not_disturb_the_bevel_it_lands_on() {
382 // `outline` has its own property, so unlike the invalid ring there is
383 // no bevel to restate beside it and nothing to keep in agreement.
384 let opts = Emit::default();
385 let css = focus_rule("button", Depth::Raised, &opts);
386 assert!(css.contains("outline: 2px solid var(--focus-ring)"));
387 assert!(!css.contains("box-shadow"), "the ring restated the bevel");
388 }
389
390 #[test]
391 fn a_well_takes_the_ring_inside_and_a_raised_surface_outside() {
392 // One ring, placed by depth. The offset comes off `Depth::bevel` and
393 // not off a per-component choice, which is what gave three apps three
394 // different rings.
395 let opts = Emit::default();
396 assert!(focus_rule("field", Depth::Well, &opts).contains("outline-offset: calc(-1 * 2px)"));
397 assert!(focus_rule("button", Depth::Raised, &opts).contains("outline-offset: 2px"));
398 // Nothing to sit inside of, so it sits outside.
399 assert!(focus_rule("badge", Depth::Sunken, &opts).contains("outline-offset: 2px"));
400
401 // And the ring is not the bevel. Reusing border_width emitted a 1px
402 // ring that every consumer had already overridden.
403 assert_ne!(opts.focus_width, opts.border_width);
404 }
405
406 #[test]
407 fn hover_is_gated_on_capability_and_the_keyboard_path_is_not() {
408 // goingson's section 60 exists only to take back the hover state this
409 // crate handed it. Gating at the source is what deletes that section
410 // in all three apps rather than having each fight for it.
411 let css = stylesheet(&Emit::default());
412 let condition = format!("@media {}", Density::Pointer.media_condition());
413 assert!(css.contains(&condition));
414
415 // What is gated is every hover state the surfaces carry. The row's
416 // actions used to be the other half of this test and are not gated any
417 // more, because they are not hidden any more: a rule that reveals
418 // nothing needs no capability answer.
419 let gated: Vec<&str> = css.lines().filter(|line| line.contains(":hover")).collect();
420 assert!(!gated.is_empty(), "{css}");
421 for line in gated {
422 let indent = line.len() - line.trim_start().len();
423 assert!(indent > 4, "an ungated hover rule: {line}");
424 }
425 assert!(!css.contains(".row:hover"), "{css}");
426 }
427
428 #[test]
429 fn the_capability_answer_is_asked_for_and_not_assumed() {
430 // Both halves come from the crates that own them. If `makeover-touch`
431 // ever says a fingertip has hover, this stops gating on its own.
432 assert!(!Affordance::Hover.available(Density::Touch, SizeClass::Compact));
433 assert!(Affordance::Hover.available(Density::Pointer, SizeClass::Compact));
434 assert_eq!(hover_condition(), Some(Density::Pointer.media_condition()));
435
436 // And the size class passed to that call is not a claim about width.
437 assert!(Affordance::Hover.reads_density());
438 for size in [SizeClass::Compact, SizeClass::Medium, SizeClass::Expanded] {
439 assert!(!Affordance::Hover.available(Density::Touch, size));
440 }
441 }
442
443 #[test]
444 fn hover_resolves_against_the_token_makeover_already_derives() {
445 let css = interactive_rules("card", Depth::Raised, &Emit::default());
446 assert!(css.contains(".card:hover {"));
447 assert!(css.contains("background: var(--hover-surface)"));
448 // Not the app's choice, which was --surface-overlay.
449 assert!(!css.contains("surface-overlay"));
450 }
451
452 #[test]
453 fn a_badge_gets_no_edge_and_no_fill() {
454 // Decision 2, and the one visible redesign in phase A. Token::Badge is
455 // Flat: an edge on a label says it can be pressed.
456 let css = token_rules(&Emit::default());
457 let badge = css
458 .lines()
459 .skip_while(|l| !l.starts_with(".badge {"))
460 .take_while(|l| !l.starts_with('}'))
461 .collect::<Vec<_>>()
462 .join("\n");
463 assert!(!badge.contains("box-shadow"), "badge kept an edge: {badge}");
464 assert!(!badge.contains("background"), "badge kept a fill: {badge}");
465 assert_eq!(Token::Badge.depth(false), Depth::Flat);
466 assert_eq!(Token::Badge.depth(true), Depth::Flat);
467 }
468
469 #[test]
470 fn a_badge_carries_a_tone_and_neutral_is_the_bare_class() {
471 let css = token_rules(&Emit::default());
472 // Neutral is the absence of a status, not a status named "none".
473 assert!(css.contains(".badge {\n color: var(--content-muted);"));
474 assert!(!css.contains("data-tone=\"content-muted\""));
475 for tone in ["info", "success", "warning", "danger"] {
476 assert!(
477 css.contains(&format!(".badge[data-tone=\"{tone}\"]")),
478 "missing tone {tone}"
479 );
480 assert!(css.contains(&format!("color: var(--{tone})")));
481 }
482 }
483
484 #[test]
485 fn a_chip_is_raised_and_latches_into_a_well() {
486 let css = token_rules(&Emit::default());
487 assert!(css.contains(".chip {"));
488 assert!(css.contains(".chip.latched {"));
489 assert!(css.contains(".chip:active {"));
490 // The whole difference from a badge: it answers a click.
491 assert!(Token::Chip { removable: false }.interactive());
492 assert!(!Token::Badge.interactive());
493 }
494
495 #[test]
496 fn only_a_tab_comes_forward_when_chosen() {
497 // The folder semantic. Collapsing the three selectors would lose it.
498 let css = selector_rules(&Emit::default());
499 assert!(css.contains(".tab.chosen {"));
500 assert!(css.contains(".segment.chosen {"));
501 assert!(css.contains(".toggle.chosen {"));
502 assert_eq!(Selector::Tabs.chosen(), Depth::Raised);
503 assert_eq!(Selector::Segmented.chosen(), Depth::Well);
504 assert_eq!(Selector::Toggle.chosen(), Depth::Well);
505
506 let tab = css
507 .lines()
508 .skip_while(|l| !l.starts_with(".tab.chosen {"))
509 .take_while(|l| !l.starts_with('}'))
510 .collect::<Vec<_>>()
511 .join("\n");
512 assert!(
513 tab.contains("var(--bevel-raised)"),
514 "tab was held in: {tab}"
515 );
516 }
517
518 #[test]
519 fn an_unchosen_tab_recedes_without_looking_picked() {
520 let css = selector_rules(&Emit::default());
521 // Recessed by colour and given no edge. An edge would make every option
522 // look picked; flat would leave the chosen one nothing to come forward
523 // from, which is the gap makeover-layout 0.3.0 closed.
524 assert!(
525 css.contains(".tab {\n background: var(--surface-sunken);\n}"),
526 "unchosen tab is not recessed: {css}"
527 );
528 assert_eq!(Selector::Tabs.unchosen(), Depth::Sunken);
529 assert!(css.contains(".tab:hover {"));
530 }
531
532 #[test]
533 fn a_segment_stands_up_so_the_chosen_one_can_be_held_in() {
534 // The inverse of the tab, and why the three selectors are not one
535 // member with a flag.
536 let css = selector_rules(&Emit::default());
537 assert!(css.contains(".segment {\n background: var(--surface-raised);"));
538 assert_eq!(Selector::Segmented.unchosen(), Depth::Raised);
539 assert_eq!(Selector::Segmented.chosen(), Depth::Well);
540 }
541
542 #[test]
543 fn a_rows_actions_are_shown_at_rest() {
544 let css = row_rules(&Emit::default());
545
546 // The hover reveal is gone, and with it every escape it needed. What
547 // it hid was hidden from pointer users alone, who are the ones
548 // scanning a list to learn what can be done to a row.
549 assert!(!css.contains("opacity"), "{css}");
550 assert!(!css.contains("pointer-events"), "{css}");
551 assert!(!css.contains(":hover"), "{css}");
552 assert!(!css.contains(":focus-within"), "{css}");
553
554 // Nor is it hidden any other way. `display: none` would reflow the row
555 // and `visibility: hidden` would take the actions out of the focus
556 // order; the point is that neither is reached for.
557 assert!(!css.contains("display: none"), "{css}");
558 assert!(!css.contains("visibility:"), "{css}");
559 }
560
561 #[test]
562 fn a_figures_tone_lands_on_the_delta_when_there_is_one() {
563 // 0.13.0. The delta is the part that reads as good or bad; the number
564 // itself is an ordinary fact. A figure with no delta has nowhere else to
565 // put the colour, so the value takes it, and `:has` is what lets one
566 // attribute mean both without the emitter choosing an element.
567 let css = stylesheet(&Emit::default());
568
569 assert!(
570 css.contains(".figure[data-tone=\"success\"] > .figure-change"),
571 "{css}"
572 );
573 assert!(
574 css.contains(".figure[data-tone=\"success\"]:not(:has(> .figure-change)) > .figure-value"),
575 "{css}"
576 );
577 // The caption is the noun and never takes the tone.
578 assert!(
579 !css.contains("[data-tone=\"success\"] > .figure-caption"),
580 "{css}"
581 );
582 }
583
584 #[test]
585 fn the_three_text_parts_take_their_intents_and_actions_inherits() {
586 let css = row_rules(&Emit::default());
587 assert!(css.contains(".row-primary {\n color: var(--content);"));
588 assert!(css.contains(".row-secondary {\n color: var(--content-secondary);"));
589 assert!(css.contains(".row-meta {\n color: var(--content-muted);"));
590 // Actions carry controls, not text. Pinning the colour it would inherit
591 // anyway is louder than saying nothing.
592 assert!(!css.contains(".row-actions {\n color:"));
593 }
594
595 #[test]
596 fn the_token_strip_takes_no_colour_of_its_own() {
597 // makeover-layout 0.9.0. A token carries its own tone, so a colour on
598 // the strip would be a rule fighting the things sitting in it -- the
599 // same reasoning as actions, reached for a different reason.
600 let css = row_rules(&Emit::default());
601 assert!(!css.contains(".row-tokens {\n color:"));
602 }
603
604 #[test]
605 fn an_unknown_row_part_renders_plainly_rather_than_failing_to_build() {
606 // What `#[non_exhaustive]` bought and what it cost. `part_class` can no
607 // longer be exhaustive, so a member added upstream lands as a bare
608 // class with no rule instead of stopping the build. Asserting the
609 // fallback exists is what keeps it from being written as `unreachable!`
610 // by someone who reads the match as closed.
611 assert_eq!(part_class(RowPart::Tokens), "row-tokens");
612 assert_eq!(part_class(RowPart::Meta), "row-meta");
613 }
614
615 #[test]
616 fn a_link_takes_the_action_colour_the_theme_actually_defines() {
617 // `--action-primary` shipped here for months and no theme has ever
618 // defined it, so every `.link` dropped its colour declaration outright
619 // and fell back to inherited text. Nothing caught it because the sheet
620 // is valid CSS either way; MNW's no-undefined-token lint is what found
621 // it, 2026-08-14. The hover arm two lines below was always `--action-hover`,
622 // which is what makes the typo legible in hindsight.
623 let css = link_rules(&Emit::default());
624 assert!(css.contains("color: var(--action);"));
625 assert!(!css.contains("--action-primary"));
626 assert!(css.contains("color: var(--action-hover);"));
627 }
628
629 #[test]
630 fn the_progress_trough_is_a_well() {
631 let css = progress_rules(&Emit::default());
632 assert!(css.contains(".progress {"));
633 assert!(css.contains("box-shadow: var(--bevel-inset)"));
634 assert!(css.contains(".progress > .progress-fill {"));
635 assert!(css.contains("background: var(--action)"));
636 // A bare `.fill` would catch things that have nothing to do with
637 // progress once the sheet lands unprefixed.
638 assert!(!css.contains("> .fill "));
639 }
640
641 #[test]
642 fn a_progress_bar_can_carry_a_tone_and_defaults_to_action() {
643 let css = progress_rules(&Emit::default());
644 // Untoned is --action, not Tone::Neutral's content-muted: a bar with no
645 // status is still reporting progress, and muted would read as disabled.
646 assert!(css.contains(".progress > .progress-fill {\n background: var(--action);"));
647 assert!(!css.contains("progress-fill {\n color: var(--content-muted)"));
648 for tone in ["info", "success", "warning", "danger"] {
649 assert!(
650 css.contains(&format!(".progress > .progress-fill[data-tone=\"{tone}\"]")),
651 "missing progress tone {tone}"
652 );
653 }
654 // goingson's two live cases, which is why the tones are emitted at all.
655 assert!(css.contains("[data-tone=\"success\"] {\n background: var(--success);"));
656 assert!(css.contains("[data-tone=\"danger\"] {\n background: var(--danger);"));
657 }
658
659 #[test]
660 fn no_scrollbar_track_is_emitted() {
661 // Decision 3's negative half. It was on the phase A list and came off;
662 // this is what stops it drifting back in.
663 let css = stylesheet(&Emit::default());
664 assert!(!css.contains("scrollbar"));
665 assert!(!css.contains("::-webkit"));
666 }
667
668 #[test]
669 fn an_invalid_field_is_ringed_without_being_lit() {
670 let css = surface_rules(&Emit::default());
671 assert!(css.contains(".field {"));
672 // The ARIA attribute, not a class: one fact, read by both the visual
673 // and the accessible state, so they cannot drift.
674 assert!(css.contains(".field[aria-invalid=\"true\"] {"));
675 assert!(!css.contains(".field.invalid"));
676 // A flat ring: this edge says "wrong", and a two-tone bevel would have
677 // it say "raised" at the same time.
678 assert!(css.contains("0 0 0 1px var(--danger)"));
679 }
680
681 #[test]
682 fn an_invalid_field_keeps_the_well_underneath_it() {
683 // box-shadow is not additive. A lone ring replaces the bevel and drops
684 // the well out from under the field, which is what this emitted before
685 // 0.5.0 and is the whole reason the rule composes.
686 let css = surface_rules(&Emit::default());
687 let invalid = css
688 .lines()
689 .skip_while(|l| !l.starts_with(".field[aria-invalid"))
690 .take_while(|l| !l.starts_with('}'))
691 .collect::<Vec<_>>()
692 .join("\n");
693 assert!(
694 invalid.contains("var(--bevel-inset)"),
695 "the well was dropped: {invalid}"
696 );
697 assert!(invalid.contains("var(--danger)"));
698 }
699
700 #[test]
701 fn button_and_card_come_out_identical_by_construction() {
702 // The duplication phase A deletes. They are the same composition, so
703 // the only honest way to emit both is from one call.
704 let opts = Emit::default();
705 let css = surface_rules(&opts);
706 assert_eq!(
707 depth_declarations(Depth::Raised),
708 depth_declarations(Depth::Raised)
709 );
710 assert!(css.contains(".button {"));
711 assert!(css.contains(".card {"));
712 assert_eq!(
713 interactive_rules("button", Depth::Raised, &Emit::default()).replace("button", "card"),
714 interactive_rules("card", Depth::Raised, &Emit::default())
715 );
716 }
717
718 #[test]
719 fn a_prefix_reaches_the_component_classes_too() {
720 let opts = Emit {
721 class_prefix: "mo-",
722 ..Emit::default()
723 };
724 let css = stylesheet(&opts);
725 for name in [
726 "mo-button",
727 "mo-card",
728 "mo-field",
729 "mo-badge",
730 "mo-chip",
731 "mo-tab",
732 "mo-row-primary",
733 "mo-progress",
734 "mo-progress-fill",
735 ] {
736 assert!(css.contains(&format!(".{name}")), "unprefixed: {name}");
737 }
738 // The bare names must be gone entirely, or a prefixed build still
739 // collides with the app's own stylesheet.
740 assert!(!css.contains(".button {"));
741 assert!(!css.contains(".card {"));
742 assert!(!css.contains(".badge {"));
743 }
744
745 #[test]
746 fn the_class_a_renderer_puts_on_an_option_is_the_one_the_rules_key_off() {
747 // `option_class` is the contract a screen renderer writes markup
748 // against, and the rules below are the other half of it. They come off
749 // one mapping now, so this asserts the mapping is the one that reaches
750 // the stylesheet rather than that two lists still agree.
751 let css = stylesheet(&Emit::default());
752 for selector in [Selector::Tabs, Selector::Segmented, Selector::Toggle] {
753 let name = option_class(selector);
754 assert!(css.contains(&format!(".{name} {{")), "{name}: {css}");
755 assert!(css.contains(&format!(".{name}.chosen {{")), "{name}: {css}");
756 }
757 assert_eq!(option_class(Selector::Tabs), "tab");
758 }
759
760 #[test]
761 fn the_caret_brings_its_own_gap_and_is_the_glyph_the_description_names() {
762 let css = stylesheet(&Emit::default());
763
764 // The space is inside the glyph, which is what the other two renderers
765 // write. Emitted bare, every consumer has to add it back, and the
766 // obvious way to add it -- `content` in an app stylesheet, which is
767 // unlayered and so outranks this sheet -- deletes the caret instead.
768 assert!(css.contains("content: \" \\25B2\";"), "{css}");
769 assert!(css.contains("content: \" \\25BC\";"), "{css}");
770 assert!(!css.contains("content: \"\\2"), "{css}");
771
772 // The arrows this renderer used to draw alone are gone. Composition
773 // rather than agreement: the glyph comes from `Sort::glyph`, so a
774 // fourth spelling cannot appear here without appearing everywhere.
775 assert!(!css.contains("2191") && !css.contains("2193"), "{css}");
776 assert!(css.contains(&css_escape(Sort::Ascending.glyph())), "{css}");
777
778 // Three states, three tones. An idle sortable heading draws its caret
779 // now rather than reserving a hidden box for it, so there is no
780 // visibility to order and no reflow left to guard against; what
781 // separates the states is the colour, and the sorted arms come after
782 // the idle one because the specificity is the same.
783 let idle = css
784 .find(".table-heading[data-sortable]::after")
785 .expect("the idle caret is emitted");
786 let sorted = css
787 .find(".table-heading[aria-sort=\"ascending\"]::after")
788 .expect("the ascending caret is emitted");
789 assert!(idle < sorted, "{css}");
790 assert!(
791 css[idle..sorted].contains("color: var(--content-secondary);"),
792 "{css}"
793 );
794 assert!(css[sorted..].contains("color: var(--content);"), "{css}");
795 assert!(!css.contains("visibility: hidden;"), "{css}");
796 }
797
798 #[test]
799 fn a_destructive_button_has_somewhere_for_its_tone_to_land() {
800 let css = component_rules(&Emit::default());
801
802 for tone in [Tone::Info, Tone::Success, Tone::Warning, Tone::Danger] {
803 assert!(
804 css.contains(&format!(".button[data-tone=\"{}\"]", tone.token())),
805 "{css}"
806 );
807 }
808 // Colour, not a fill. A red surface is an app's decision about emphasis.
809 assert!(!css.contains(".button[data-tone=\"danger\"] {\n background"));
810 }
811
812 #[test]
813 fn the_table_model_is_scoped_to_the_table_but_the_caret_is_not() {
814 // goingson's task headings are `.table-heading` inside a CSS grid.
815 // They want the sort caret and the sortable cursor; they do not want
816 // `display: table-cell`, which a grid item blockifies away anyway.
817 // Scoping the one and not the other is what separates them.
818 let css = component_rules(&Emit::default());
819
820 assert!(
821 css.contains(".table .table-heading,\n.table .cell {\n display: table-cell;"),
822 "{css}"
823 );
824 assert!(
825 !css.contains(".table-heading,\n.cell {\n display: table-cell;"),
826 "the table model is still unscoped: {css}"
827 );
828
829 let states = state_rules(&Emit::default());
830 assert!(
831 states.contains(".table-heading[aria-sort"),
832 "the caret got scoped along with the model: {states}"
833 );
834 }
835
836 #[test]
837 fn a_figure_value_is_the_thing_itself_and_a_badge_is_quiet() {
838 // Both used to read `Tone::Neutral.token()`, which answered
839 // `content-muted`, so the headline number sat at the colour of its own
840 // caption. Neutral answers `content` now; each site states its own
841 // claim rather than borrowing one from the status axis.
842 let css = component_rules(&Emit::default());
843
844 assert!(
845 css.contains(".figure > .figure-value {\n color: var(--content);"),
846 "{css}"
847 );
848 assert!(
849 css.contains(".figure > .figure-caption {\n color: var(--content-muted);"),
850 "{css}"
851 );
852 assert!(
853 css.contains(".badge {\n color: var(--content-muted);"),
854 "{css}"
855 );
856 }
857
858 #[test]
859 fn a_described_list_is_not_a_bulleted_list() {
860 let css = component_rules(&Emit::default());
861 assert!(css.contains(".list {\n list-style: none;"), "{css}");
862 }
863
864 #[test]
865 fn a_table_lays_itself_out_without_being_told_its_columns() {
866 // The whole point of the CSS table. A described table's columns are
867 // known at render time, so anything the stylesheet has to be told about
868 // them would have to travel with the markup.
869 let css = component_rules(&Emit::default());
870
871 assert!(css.contains(".table {\n display: table;"), "{css}");
872 assert!(css.contains("display: table-row;"), "{css}");
873 assert!(css.contains("display: table-cell;"), "{css}");
874 assert!(!css.contains("grid-template-columns"), "{css}");
875 }
876
877 #[test]
878 fn a_control_in_a_cell_is_not_painted_as_text() {
879 // The point of makeover-layout 0.14.0's CellPart, and the table-side
880 // twin of `the_three_text_parts_take_their_intents_and_actions_inherits`
881 // above. One `.cell` and one content colour meant a button in a cell
882 // inherited it.
883 let css = table_rules(&Emit::default());
884
885 assert!(
886 css.contains(".cell-value {\n color: var(--content);"),
887 "{css}"
888 );
889 assert!(!css.contains(".cell-actions {\n color:"), "{css}");
890 assert!(!css.contains(".cell-tokens {\n color:"), "{css}");
891 assert!(!css.contains(".cell-link {\n color:"), "{css}");
892
893 // The colour is on the part that is text, never on the container. On
894 // `.cell` it would cascade into the three parts that are not text,
895 // which is the bug written as one rule.
896 assert!(!css.contains(".cell {\n color:"), "{css}");
897 }
898
899 #[test]
900 fn an_unknown_cell_part_renders_plainly_rather_than_failing_to_build() {
901 // `part_class`'s obligation, taken on for the table side too. CellPart
902 // is `#[non_exhaustive]`, so a member added upstream must land as a
903 // bare class rather than as a build that stops.
904 assert_eq!(cell_part_class(CellPart::Value), "cell-value");
905 assert_eq!(cell_part_class(CellPart::Actions), "cell-actions");
906 }
907
908 #[test]
909 fn a_column_drops_by_its_priority_and_never_by_its_position() {
910 let css = component_rules(&Emit::default());
911
912 // Optional goes at the narrowest class and secondary goes with it,
913 // which is `kept_at`'s cutoff walk said as two queries.
914 let compact = css
915 .find(&format!("@media {}", SizeClass::Compact.media_condition()))
916 .expect("a compact query");
917 let medium = css
918 .find(&format!("@media {}", SizeClass::Medium.media_condition()))
919 .expect("a medium query");
920 assert!(css[compact..].contains(".cell-drops-next"), "{css}");
921 assert!(!css[medium..].contains(".cell-drops-next"), "{css}");
922
923 // Essential columns are never mentioned, because not being mentioned is
924 // already what never dropping means.
925 assert!(!css.contains(".cell-keeps"), "{css}");
926
927 // And nothing counts. `nth-child` is the bug the priority vocabulary
928 // exists to end.
929 assert!(!css.contains("nth-child"), "{css}");
930 }
931
932 #[test]
933 fn a_track_places_by_custom_property_and_never_by_a_size() {
934 let css = track_rules(&Emit::default());
935
936 // Placement arrives from the caller, computed once by Track::fraction.
937 // If either of these becomes a literal, three renderers have started
938 // disagreeing about where 09:30 is.
939 assert!(css.contains("top: var(--track-at"), "{css}");
940 assert!(css.contains("height: var(--track-for"), "{css}");
941
942 // Overlap lanes default so an entry naming neither is full width.
943 assert!(css.contains("--track-lane, 0"), "{css}");
944 assert!(css.contains("--track-lanes, 1"), "{css}");
945
946 // The refusal that matters. A slot height here would be this crate
947 // deciding how tall a quarter of an hour is, which is the thing
948 // makeover-geometry owns and the reason `.track` gets no height at all.
949 assert!(!css.contains("height: var(--track-slot"), "{css}");
950 for size in ["px", "rem", "em", "vh"] {
951 let bare = css
952 .lines()
953 .filter(|l| !l.contains("var(--"))
954 .any(|l| l.contains(size));
955 assert!(!bare, "track_rules named a {size} outside a var(): {css}");
956 }
957 }
958
959 #[test]
960 fn a_relaxed_part_clamps_and_a_tight_one_says_nothing() {
961 let css = stylesheet(&Emit::default());
962 assert!(css.contains(".row-relaxed {"));
963 }
964
965 /// The done condition: a row carrying a level indents in a browser with no
966 /// app-authored CSS.
967 ///
968 /// quasi-webview emits `row-nested` with `--row-depth` on every described
969 /// hierarchy. Without a rule reading it, a described outline is a flat list
970 /// with chevrons in it.
971 #[test]
972 fn a_nested_row_indents_by_its_level_and_an_app_can_say_what_a_level_is_worth() {
973 let css = stylesheet(&Emit::default());
974
975 assert!(css.contains(".row-nested {"), "{css}");
976 assert!(
977 css.contains("padding-inline-start: calc(var(--row-depth, 0) * var(--row-indent, 1.5ch));"),
978 "{css}"
979 );
980 // The magnitude is a custom property with a fallback, which is
981 // `--awaiting-gap`'s shape: what a level IS stays the description's and
982 // what it is WORTH is this renderer's, overridable by an app. Padding
983 // rather than margin, so the indent is inside the box a selection
984 // shades.
985 assert!(
986 !css.contains("margin-inline-start: calc(var(--row-depth"),
987 "{css}"
988 );
989
990 // The branch and its chevron, emitted and unstyled for the same reason.
991 assert!(css.contains(".row-branch {"), "{css}");
992 assert!(css.contains(".row-disclose {"), "{css}");
993
994 for name in ["row-nested", "row-branch", "row-disclose"] {
995 assert!(
996 crate::vocabulary::names(&Emit::default()).contains(name),
997 "{name} is declared"
998 );
999 }
1000 assert!(css.contains("-webkit-line-clamp: 2;"));
1001 // The count is `Flow`'s, not this crate's. If the tier ever means three
1002 // lines, this fails here rather than in an app.
1003 assert!(css.contains(&format!("line-clamp: {};", Flow::Relaxed.lines())));
1004 // Tight gets no rule at all: one line is what a run already does, and a
1005 // class per part saying so is a declaration that changes nothing.
1006 assert!(!css.contains("row-tight"));
1007 assert_eq!(crate::list::flow_class(Flow::Relaxed), Some("row-relaxed"));
1008 assert_eq!(crate::list::flow_class(Flow::Tight), None);
1009 // Emitted, therefore checkable: an app's dead-vocabulary seal and the
1010 // overlap check both read `vocabulary::names`, so a class the renderer
1011 // can write and that list does not carry is invisible to both.
1012 assert!(crate::vocabulary::names(&Emit::default()).contains("row-relaxed"));
1013 }
1014
1015 #[test]
1016 fn the_two_kinds_of_wait_stop_rendering_identically() {
1017 // The state `d43ea1c5` fixes: the emitter had been writing
1018 // `data-awaiting` for months and nothing styled either value, so a
1019 // measured wait and an unmeasured one drew the same nothing.
1020 let css = stylesheet(&Emit::default());
1021 assert!(css.contains("[data-awaiting]::after"), "{css}");
1022 assert!(
1023 css.contains("[data-awaiting][aria-busy=\"true\"]::after"),
1024 "the mark is drawn only while something is actually waiting"
1025 );
1026 assert!(
1027 css.contains("[data-awaiting=\"determinate\"][aria-busy=\"true\"]::after"),
1028 "the measured half is its own drawing"
1029 );
1030 }
1031
1032 #[test]
1033 fn the_blink_takes_its_cadence_and_never_names_one() {
1034 // Three renderers draw this mark. A number written here would be a
1035 // second heartbeat for one wait.
1036 let css = stylesheet(&Emit::default());
1037 assert!(
1038 css.contains("calc(var(--cadence-activity) * 2)"),
1039 "a half-period doubled, not a literal"
1040 );
1041 assert!(!css.contains("500ms"), "{css}");
1042 }
1043
1044 #[test]
1045 fn a_bar_nobody_is_counting_is_empty_rather_than_full() {
1046 // Rule 1. The share defaults to zero, so a determinate control with no
1047 // binder watching bytes draws a trough. A default of 1 would be the
1048 // confidently-wrong drawing the rule exists to forbid.
1049 let css = stylesheet(&Emit::default());
1050 assert!(css.contains("var(--awaiting-share, 0)"), "{css}");
1051 }
1052
1053 #[test]
1054 fn motion_off_leaves_the_mark_lit_because_the_keyframes_do_the_dimming() {
1055 // `makeover_timing::reduced_motion_css` sets `--cadence-activity: 0ms`,
1056 // and a zero-length animation leaves the element in its base style
1057 // rather than at its last keyframe. So the base has to be the lit one.
1058 // The inverted spelling would blank the mark for a reader who asked for
1059 // less motion, which answers a request nobody made.
1060 let css = stylesheet(&Emit::default());
1061 let busy = css
1062 .split("[data-awaiting][aria-busy=\"true\"]::after {")
1063 .nth(1)
1064 .expect("the busy rule");
1065 let busy = busy.split('}').next().expect("its body");
1066 assert!(
1067 busy.contains("background: var(--action);"),
1068 "the base state is lit: {busy}"
1069 );
1070 }
1071
1072 #[test]
1073 fn the_whole_sheet_still_names_every_colour() {
1074 // The crate's founding property, asserted over the component layer and
1075 // not only the primitives.
1076 let css = stylesheet(&Emit::default());
1077 assert!(!css.contains('#'));
1078 assert!(!css.contains("rgb"));
1079 for line in css.lines() {
1080 // Declarations only: a selector or an at-rule can carry a colon of
1081 // its own (`:root`, `:hover`, `@media (hover: hover)`) and declares
1082 // nothing. Keyed on the trailing semicolon rather than on leading
1083 // indentation, which only ever worked as a proxy for nesting depth
1084 // and stopped when the sheet gained a cascade layer around it.
1085 let trimmed = line.trim();
1086 if !trimmed.ends_with(';') {
1087 continue;
1088 }
1089 let Some((_, value)) = trimmed.split_once(": ") else {
1090 continue;
1091 };
1092 if value.contains("var(--") {
1093 continue;
1094 }
1095 // Everything left has to be a keyword, a number or a
1096 // caller-supplied length, never a colour.
1097 //
1098 // The length arm is what the comment above always claimed and the
1099 // list never covered: `border_width` arrives from `Emit` and lands
1100 // bare in the focus ring's offset, where the bevel had only ever
1101 // used it inside an `inset` shadow.
1102 let opts = Emit::default();
1103 assert!(
1104 value.contains("inset")
1105 || value.contains(opts.border_width)
1106 || value.contains(opts.focus_width)
1107 // 0.12.0's two: a sortable header is a control and says so
1108 // with the pointer, and the caret is this renderer's own
1109 // expression of `aria-sort`. Neither is a colour, which is
1110 // what this test is actually about, and neither is a size,
1111 // which is the other thing this crate must not name. The
1112 // leading space inside the glyph is the same thing the other
1113 // two renderers write into theirs, so it is part of the
1114 // caret rather than spacing this crate decided on.
1115 || value
1116 .trim_start_matches('"')
1117 .trim_start()
1118 .starts_with("\\2")
1119 || matches!(
1120 value.trim_end_matches(';'),
1121 "0" | "1"
1122 // The wait's mark and bar, 0.60.0. An empty
1123 // `content` is what brings a pseudo-element into
1124 // existence with nothing in it, and `step-end`
1125 // is an easing: the blink is two states, not a
1126 // slide between them. Neither is a colour, and
1127 // neither is a magnitude.
1128 | "\"\""
1129 // Where the mark sits on the line it joins.
1130 // Alignment is structure, the same way `display`
1131 // is, and `baseline` is the initial value said out
1132 // loud so a host stylesheet cannot leave it
1133 // wherever an earlier rule put it.
1134 | "baseline"
1135 | "inline-block"
1136 | "none"
1137 | "auto"
1138 | "not-allowed"
1139 | "pointer"
1140 // The caret's reserved box. Visibility is presence,
1141 // not magnitude and not colour.
1142 | "hidden"
1143 | "visible"
1144 // The link's two signals. `underline` is a line and
1145 // `inherit` defers to whatever the app set, so
1146 // neither names a colour or a magnitude.
1147 | "underline"
1148 | "inherit"
1149 // The table frame. `display` is structure and not a
1150 // size; `nowrap` is what makes a content column
1151 // content. The two widths are the awkward pair and
1152 // they are still not sizes: `100%` is "all of
1153 // whatever you were given" and `1%` is the CSS
1154 // table idiom for "shrink to fit", which is a
1155 // behaviour spelled as a number because CSS has no
1156 // keyword for it. Neither names a magnitude, which
1157 // is the thing this crate leaves to
1158 // makeover-geometry.
1159 | "table"
1160 | "table-row"
1161 | "table-cell"
1162 | "nowrap"
1163 | "100%"
1164 | "1%"
1165 // The time axis, 0.42.0. `position` is the one
1166 // property whose whole job is where a thing sits,
1167 // which is exactly what this crate spent its life
1168 // refusing to say -- so it is worth being exact
1169 // about why these two are not that refusal
1170 // breaking.
1171 //
1172 // Neither names a magnitude. `relative` says the
1173 // track is what its entries resolve against, and
1174 // `absolute` says an entry is placed rather than
1175 // flowed. *Where* each entry lands is
1176 // `--track-at` and `--track-for`, custom
1177 // properties the caller sets from
1178 // `Track::fraction`, and they are skipped by the
1179 // `var(--` arm above like every other value this
1180 // crate refuses to decide.
1181 //
1182 // The rule that would break the refusal is a slot
1183 // height, and there is none: the track's height is
1184 // the app's, so the percentages have something to
1185 // resolve against and this crate still never says
1186 // how tall a day is.
1187 | "relative"
1188 | "absolute"
1189 // A run's five, 0.49.0. `flex`, `wrap` and
1190 // `center` are structure and alignment, the same
1191 // reading `table` gets: which way members are laid
1192 // out and how they line up, never how much of
1193 // anything.
1194 //
1195 // The two intrinsic keywords are the interesting
1196 // pair and they are the opposite of a size. A
1197 // magnitude is a number somebody chose;
1198 // `min-content` and `max-content` are the browser
1199 // being asked what the members themselves come to,
1200 // which is the derived minimum the room ruling
1201 // requires and the reason no breakpoint appears
1202 // anywhere in these rules. `1 1 max-content` is
1203 // grow, shrink and that basis, so its two digits
1204 // are ratios rather than lengths.
1205 | "flex"
1206 | "wrap"
1207 | "center"
1208 | "min-content"
1209 | "1 1 max-content"
1210 // A run member that absorbs what is left, 0.74.0.
1211 // Grow, shrink and a zero basis: the zero is what
1212 // makes several fills divide the room equally
1213 // rather than dividing the leftovers in proportion
1214 // to their contents. Three ratios and no length.
1215 | "1 1 0"
1216 // A menu run's overflow control, 0.64.0.
1217 // `column` and `stretch` are the same reading
1218 // `flex` and `center` get one line up: which way
1219 // the shed members stack inside the panel and how
1220 // they line up across it. Neither is a magnitude.
1221 //
1222 // `100%` is already above and reused here as the
1223 // panel's `inset-block-start`, which is "the whole
1224 // of the control it hangs from" rather than a
1225 // distance anybody picked.
1226 | "column"
1227 | "stretch"
1228 // A picture's three, 0.36.0. `block` is structure
1229 // for the reason `table` is: an inline image sits
1230 // on the baseline and carries a descender's worth
1231 // of space under it, which is a fact about
1232 // replaced elements rather than a size this crate
1233 // chose. `cover` and `contain` are `Fit`'s two
1234 // named members reaching CSS unchanged, which is
1235 // an intent arriving rather than a value being
1236 // picked.
1237 | "block"
1238 | "cover"
1239 | "contain"
1240 // A relaxed part's three, and the third is the
1241 // awkward one. `-webkit-box` and `vertical` are
1242 // structure: they say the part is a box of lines
1243 // stacked downward, which is the only way CSS lets
1244 // anyone ask for a clamp at all.
1245 //
1246 // `2` is a count of lines, not a length. The
1247 // distinction this crate holds is between naming a
1248 // magnitude -- a padding, a height, a font size,
1249 // all of which belong to makeover-geometry -- and
1250 // naming how many of something there are. A line's
1251 // height is still the app's, so two lines is
1252 // whatever two of the app's lines come to, and
1253 // nothing here decides how tall that is. It is also
1254 // not a value picked here: it is `Flow::Relaxed`'s
1255 // own answer arriving unchanged, the same way
1256 // `cover` and `contain` are `Fit`'s.
1257 | "-webkit-box"
1258 | "vertical"
1259 | "2"
1260 ),
1261 "unrecognised literal value: {line}"
1262 );
1263 }
1264 }
1265
1266 #[test]
1267 fn flat_emits_nothing_at_all() {
1268 assert_eq!(depth_class(Depth::Flat, &Emit::default()), None);
1269 assert!(!depth_rules(&Emit::default()).contains("flat"));
1270 }
1271
1272 #[test]
1273 fn a_prefix_namespaces_every_class() {
1274 let opts = Emit {
1275 class_prefix: "mo-",
1276 ..Emit::default()
1277 };
1278 let css = depth_rules(&opts);
1279 assert!(css.contains(".mo-raised {"));
1280 assert!(css.contains(".mo-well {"));
1281 assert!(!css.contains(".raised {"));
1282 }
1283
1284 #[test]
1285 fn the_border_width_is_the_callers() {
1286 let opts = Emit {
1287 border_width: "2px",
1288 ..Emit::default()
1289 };
1290 assert!(bevel_shadow(Bevel::Raised, &opts).contains("inset 2px 2px 0"));
1291 }
1292
1293 #[test]
1294 fn edges_agree_with_the_description() {
1295 // Not a tautology: it is the guard that a CSS-shaped convenience never
1296 // quietly reverses which side is lit.
1297 let (tl, br) = Bevel::Raised.edges();
1298 assert_eq!(tl.token(), Edge::Light.token());
1299 assert_eq!(br.token(), Edge::Dark.token());
1300 }
1301