| 403 |
403 |
|
}
|
| 404 |
404 |
|
|
| 405 |
405 |
|
/// Fail the build if a hand-written stylesheet takes a property the generated
|
| 406 |
|
- |
/// one already sets on the same class.
|
|
406 |
+ |
/// one already sets -- on the same class, or on an element that carries it.
|
| 407 |
407 |
|
///
|
| 408 |
|
- |
/// The generated sheet sits in `@layer makeover`. Unlayered app CSS beats a
|
| 409 |
|
- |
/// layer by construction, whatever the specificity, so an app declaration for a
|
| 410 |
|
- |
/// property makeover already sets does not merge with it: it wins, silently,
|
| 411 |
|
- |
/// and the design system's version of that component stops applying. Both
|
| 412 |
|
- |
/// sort-caret defects found on 2026-08-11 were this, and both were live for
|
| 413 |
|
- |
/// months because nothing looked.
|
|
408 |
+ |
/// The generated sheet sits in `@layer makeover`. App CSS beats it whatever the
|
|
409 |
+ |
/// specificity, either by being unlayered or by sitting in a layer the app's
|
|
410 |
+ |
/// order statement puts after `makeover`, so an app declaration for a property
|
|
411 |
+ |
/// makeover already sets does not merge with it: it wins, silently, and the
|
|
412 |
+ |
/// design system's version of that component stops applying. Both sort-caret
|
|
413 |
+ |
/// defects found on 2026-08-11 were this, and both were live for months because
|
|
414 |
+ |
/// nothing looked.
|
|
415 |
+ |
///
|
|
416 |
+ |
/// # Two passes, because a rule can carry no class
|
|
417 |
+ |
///
|
|
418 |
+ |
/// The class pass is the original: an app `.button` against the generated
|
|
419 |
+ |
/// `.button`. It reads rules by the classes in their selectors, so a rule with
|
|
420 |
+ |
/// no class in it is invisible to it -- and `button { color: var(--content) }`
|
|
421 |
+ |
/// is exactly that. It sets the same property the generated `.button` sets, on
|
|
422 |
+ |
/// every described act in the app, and it took `.button[data-tone="danger"]`'s
|
|
423 |
+ |
/// tone with it: a destructive act rendered indistinguishable from an ordinary
|
|
424 |
+ |
/// one for months, with this check reporting nothing.
|
|
425 |
+ |
///
|
|
426 |
+ |
/// The element pass closes it. `makeover_webview::vocabulary::ELEMENT_CLASSES`
|
|
427 |
+ |
/// says which generated classes an element can carry -- CSS cannot say it, and
|
|
428 |
+ |
/// the renderer can -- and a bare element rule taking a property the design
|
|
429 |
+ |
/// system sets on one of those classes is the same defect as the class case.
|
|
430 |
+ |
///
|
|
431 |
+ |
/// Two things are not reported, both deliberately:
|
|
432 |
+ |
///
|
|
433 |
+ |
/// - A scoped rule (`.page button`). It reaches the elements inside one
|
|
434 |
+ |
/// region rather than every one of them, so whether it lands on a described
|
|
435 |
+ |
/// act depends on where that act renders. The certain case is the one this
|
|
436 |
+ |
/// reads.
|
|
437 |
+ |
/// - A property the app names on the class itself, from a rule that outranks
|
|
438 |
+ |
/// the element rule. Both are the app's and both sit in the same layer, so
|
|
439 |
+ |
/// that one contest is settled by specificity, and what reaches the design
|
|
440 |
+ |
/// system is the class rule -- which the class pass has already, reported
|
|
441 |
+ |
/// or reviewed. The rank matters: `.field` does not beat
|
|
442 |
+ |
/// `input[type="text"]`, and a handoff written as the weaker of the two is
|
|
443 |
+ |
/// a remedy that looks written and is not.
|
| 414 |
444 |
|
///
|
| 415 |
445 |
|
/// # Why properties and not class names
|
| 416 |
446 |
|
///
|
| 438 |
468 |
|
/// a claim about who owns a property, and it expires when the design system
|
| 439 |
469 |
|
/// takes the property back.
|
| 440 |
470 |
|
///
|
|
471 |
+ |
/// `allowed_elements` is the same thing one pass down: `(element, class,
|
|
472 |
+ |
/// property)` triples where a bare element rule reaching a generated class has
|
|
473 |
+ |
/// been read and kept.
|
|
474 |
+ |
///
|
|
475 |
+ |
/// The remedy is usually neither list. A later layer can hand the property back
|
|
476 |
+ |
/// with `revert-layer`, which says "whatever the design system set here, keep
|
|
477 |
+ |
/// it" on the arms makeover actually paints, and that is a statement in the
|
|
478 |
+ |
/// stylesheet rather than a note in a build script. Handoffs are not reported by
|
|
479 |
+ |
/// either pass.
|
|
480 |
+ |
///
|
| 441 |
481 |
|
/// A pair that stops colliding fails too. A licence nobody is using is where
|
| 442 |
482 |
|
/// the next real collision lands and reads as company.
|
| 443 |
483 |
|
///
|
| 458 |
498 |
|
opts: &Emit,
|
| 459 |
499 |
|
generated: &[&str],
|
| 460 |
500 |
|
allowed: &[(&str, &str)],
|
|
501 |
+ |
allowed_elements: &[(&str, &str, &str)],
|
| 461 |
502 |
|
) {
|
| 462 |
503 |
|
let frontend = frontend.as_ref();
|
| 463 |
504 |
|
let css = frontend.join("css");
|
| 468 |
509 |
|
!generated.contains(&name.as_str())
|
| 469 |
510 |
|
})
|
| 470 |
511 |
|
.collect();
|
| 471 |
|
- |
check_vocabulary_paths(&files, opts, Some(frontend), allowed);
|
|
512 |
+ |
check_vocabulary_paths(&files, opts, Some(frontend), allowed, allowed_elements);
|
| 472 |
513 |
|
}
|
| 473 |
514 |
|
|
| 474 |
515 |
|
/// [`check_vocabulary`] against a named list of files rather than a tree.
|
| 481 |
522 |
|
/// # Panics
|
| 482 |
523 |
|
///
|
| 483 |
524 |
|
/// As [`check_vocabulary`].
|
| 484 |
|
- |
pub fn check_vocabulary_files<P: AsRef<Path>>(paths: &[P], opts: &Emit, allowed: &[(&str, &str)]) {
|
|
525 |
+ |
pub fn check_vocabulary_files<P: AsRef<Path>>(
|
|
526 |
+ |
paths: &[P],
|
|
527 |
+ |
opts: &Emit,
|
|
528 |
+ |
allowed: &[(&str, &str)],
|
|
529 |
+ |
allowed_elements: &[(&str, &str, &str)],
|
|
530 |
+ |
) {
|
| 485 |
531 |
|
let paths: Vec<PathBuf> = paths.iter().map(|p| p.as_ref().to_path_buf()).collect();
|
| 486 |
|
- |
check_vocabulary_paths(&paths, opts, None, allowed);
|
|
532 |
+ |
check_vocabulary_paths(&paths, opts, None, allowed, allowed_elements);
|
| 487 |
533 |
|
}
|
| 488 |
534 |
|
|
| 489 |
535 |
|
/// The check itself. `root`, when given, is stripped from reported paths.
|
| 492 |
538 |
|
opts: &Emit,
|
| 493 |
539 |
|
root: Option<&Path>,
|
| 494 |
540 |
|
allowed: &[(&str, &str)],
|
|
541 |
+ |
allowed_elements: &[(&str, &str, &str)],
|
| 495 |
542 |
|
) {
|
| 496 |
543 |
|
let generated =
|
| 497 |
544 |
|
makeover_webview::vocabulary::declarations_by_class(&makeover_webview::stylesheet(opts));
|
| 498 |
545 |
|
let mut clashes: Vec<String> = Vec::new();
|
| 499 |
546 |
|
let mut seen: Vec<(String, String)> = Vec::new();
|
|
547 |
+ |
let mut element_clashes: Vec<String> = Vec::new();
|
|
548 |
+ |
let mut element_seen: Vec<(String, String, String)> = Vec::new();
|
| 500 |
549 |
|
|
| 501 |
550 |
|
for path in paths {
|
| 502 |
551 |
|
println!("cargo::rerun-if-changed={}", path.display());
|
| 521 |
570 |
|
clashes.push(format!(" {name} .{class} {{ {property} }}"));
|
| 522 |
571 |
|
}
|
| 523 |
572 |
|
}
|
|
573 |
+ |
|
|
574 |
+ |
// The second pass: a rule carrying no class at all, which the first one
|
|
575 |
+ |
// cannot see. What the app says about the class itself settles the
|
|
576 |
+ |
// pair, but only from a rule that outranks the element rule -- both are
|
|
577 |
+ |
// the app's and both are in the same layer, so this one contest is
|
|
578 |
+ |
// decided by specificity. `.field` does not beat `input[type="text"]`.
|
|
579 |
+ |
let mentioned = makeover_webview::vocabulary::mentions_by_class(&raw);
|
|
580 |
+ |
let by_element = makeover_webview::vocabulary::declarations_by_element(&raw);
|
|
581 |
+ |
for (element, properties) in &by_element {
|
|
582 |
+ |
for class in makeover_webview::vocabulary::classes_for_element(element, opts) {
|
|
583 |
+ |
let Some(theirs) = generated.get(&class) else {
|
|
584 |
+ |
continue;
|
|
585 |
+ |
};
|
|
586 |
+ |
for (property, rank) in properties {
|
|
587 |
+ |
if !theirs.contains(property) {
|
|
588 |
+ |
continue;
|
|
589 |
+ |
}
|
|
590 |
+ |
// A tie goes to the class rule: at equal specificity the
|
|
591 |
+ |
// later rule wins, and a remedy is written after the rule
|
|
592 |
+ |
// it remedies.
|
|
593 |
+ |
let spoken_for = mentioned
|
|
594 |
+ |
.get(&class)
|
|
595 |
+ |
.and_then(|properties| properties.get(property))
|
|
596 |
+ |
.is_some_and(|theirs| theirs >= rank);
|
|
597 |
+ |
if spoken_for {
|
|
598 |
+ |
continue;
|
|
599 |
+ |
}
|
|
600 |
+ |
element_seen.push((element.clone(), class.clone(), property.clone()));
|
|
601 |
+ |
if allowed_elements.contains(&(
|
|
602 |
+ |
element.as_str(),
|
|
603 |
+ |
class.as_str(),
|
|
604 |
+ |
property.as_str(),
|
|
605 |
+ |
)) {
|
|
606 |
+ |
continue;
|
|
607 |
+ |
}
|
|
608 |
+ |
element_clashes.push(format!(
|
|
609 |
+ |
" {name} {element} {{ {property} }} beats .{class} {{ {property} }}"
|
|
610 |
+ |
));
|
|
611 |
+ |
}
|
|
612 |
+ |
}
|
|
613 |
+ |
}
|
| 524 |
614 |
|
}
|
| 525 |
615 |
|
|
| 526 |
616 |
|
assert!(
|
| 527 |
617 |
|
clashes.is_empty(),
|
| 528 |
618 |
|
"{} hand-written declaration(s) take a property the generated stylesheet \
|
| 529 |
|
- |
already sets on the same class. App CSS is unlayered and beats \
|
| 530 |
|
- |
@layer makeover, so each of these wins over the design system \
|
| 531 |
|
- |
silently:\n{}\n\nDelete the declaration, or, if it is a deliberate pairing \
|
| 532 |
|
- |
on a different selector arm, add (class, property) to this check's \
|
| 533 |
|
- |
allowed list and say why beside it. Count the consumers before deciding \
|
| 534 |
|
- |
a divergence is worth keeping.",
|
|
619 |
+ |
already sets on the same class. App CSS wins over @layer makeover, \
|
|
620 |
+ |
whether by a later layer or by being unlayered, so each of these wins \
|
|
621 |
+ |
over the design system silently:\n{}\n\nDelete the declaration, or, if \
|
|
622 |
+ |
it is a deliberate pairing on a different selector arm, add \
|
|
623 |
+ |
(class, property) to this check's allowed list and say why beside it. \
|
|
624 |
+ |
Count the consumers before deciding a divergence is worth keeping.",
|
| 535 |
625 |
|
clashes.len(),
|
| 536 |
626 |
|
clashes.join("\n")
|
| 537 |
627 |
|
);
|
| 538 |
628 |
|
|
|
629 |
+ |
assert!(
|
|
630 |
+ |
element_clashes.is_empty(),
|
|
631 |
+ |
"{} hand-written element rule(s) take a property the generated \
|
|
632 |
+ |
stylesheet sets on a class that element carries. App CSS wins over \
|
|
633 |
+ |
@layer makeover, whether by a later layer or by being unlayered, so a \
|
|
634 |
+ |
described component rendered on one of these elements loses the \
|
|
635 |
+ |
design system's version of that property silently -- which is how a \
|
|
636 |
+ |
destructive act came to look like an ordinary one:\n{}\n\nHand the \
|
|
637 |
+ |
property back on the arms makeover paints \
|
|
638 |
+ |
(`.{{class}}:disabled {{ color: revert-layer }}`), scope the element \
|
|
639 |
+ |
rule so it stops reaching described markup, or add \
|
|
640 |
+ |
(element, class, property) to this check's allowed-elements list and \
|
|
641 |
+ |
say why beside it.",
|
|
642 |
+ |
element_clashes.len(),
|
|
643 |
+ |
element_clashes.join("\n")
|
|
644 |
+ |
);
|
|
645 |
+ |
|
| 539 |
646 |
|
let stale: Vec<&(&str, &str)> = allowed
|
| 540 |
647 |
|
.iter()
|
| 541 |
648 |
|
.filter(|(class, property)| {
|
| 548 |
655 |
|
anything. Delete the entries: an exception nobody is using is where the \
|
| 549 |
656 |
|
next real collision lands and reads as company."
|
| 550 |
657 |
|
);
|
|
658 |
+ |
|
|
659 |
+ |
let stale: Vec<&(&str, &str, &str)> = allowed_elements
|
|
660 |
+ |
.iter()
|
|
661 |
+ |
.filter(|(element, class, property)| {
|
|
662 |
+ |
!element_seen.contains(&(
|
|
663 |
+ |
(*element).to_string(),
|
|
664 |
+ |
(*class).to_string(),
|
|
665 |
+ |
(*property).to_string(),
|
|
666 |
+ |
))
|
|
667 |
+ |
})
|
|
668 |
+ |
.collect();
|
|
669 |
+ |
assert!(
|
|
670 |
+ |
stale.is_empty(),
|
|
671 |
+ |
"the allowed-elements list declares {stale:?}, which no longer collides \
|
|
672 |
+ |
with anything. Delete the entries: an exception nobody is using is \
|
|
673 |
+ |
where the next real collision lands and reads as company."
|
|
674 |
+ |
);
|
| 551 |
675 |
|
}
|
| 552 |
676 |
|
|
| 553 |
677 |
|
/// Warn when the generated vocabulary has grown dead, and fail when it grows
|
| 855 |
979 |
|
"css/styles.css",
|
| 856 |
980 |
|
"body { color: red; }\n.card { box-shadow: none; }\n",
|
| 857 |
981 |
|
);
|
| 858 |
|
- |
let err = std::panic::catch_unwind(|| check_vocabulary(&dir, &Emit::default(), &[], &[]))
|
| 859 |
|
- |
.unwrap_err();
|
|
982 |
+ |
let err =
|
|
983 |
+ |
std::panic::catch_unwind(|| check_vocabulary(&dir, &Emit::default(), &[], &[], &[]))
|
|
984 |
+ |
.unwrap_err();
|
| 860 |
985 |
|
let msg = err
|
| 861 |
986 |
|
.downcast_ref::<String>()
|
| 862 |
987 |
|
.expect("panic payload is a String");
|
| 873 |
998 |
|
"css/styles.css",
|
| 874 |
999 |
|
".task-list-container { overflow: auto; }\n.day-plan-slot { height: 1rem; }\n",
|
| 875 |
1000 |
|
);
|
| 876 |
|
- |
check_vocabulary(&dir, &Emit::default(), &[], &[]);
|
|
1001 |
+ |
check_vocabulary(&dir, &Emit::default(), &[], &[], &[]);
|
| 877 |
1002 |
|
}
|
| 878 |
1003 |
|
|
| 879 |
1004 |
|
#[test]
|
| 883 |
1008 |
|
write(&dir, "css/layout.css", &makeover_webview::stylesheet(&opts));
|
| 884 |
1009 |
|
// Without the skip this is the loudest failure possible: every class in
|
| 885 |
1010 |
|
// the vocabulary, reported as a clash with the vocabulary.
|
| 886 |
|
- |
check_vocabulary(&dir, &opts, &["layout.css"], &[]);
|
|
1011 |
+ |
check_vocabulary(&dir, &opts, &["layout.css"], &[], &[]);
|
| 887 |
1012 |
|
}
|
| 888 |
1013 |
|
|
| 889 |
1014 |
|
#[test]
|
| 896 |
1021 |
|
// Bare `.card` is the app's own class once the generated sheet writes
|
| 897 |
1022 |
|
// `.mo-card`, so this has to pass.
|
| 898 |
1023 |
|
write(&dir, "css/styles.css", ".card { box-shadow: none; }\n");
|
| 899 |
|
- |
check_vocabulary(&dir, &opts, &[], &[]);
|
|
1024 |
+ |
check_vocabulary(&dir, &opts, &[], &[], &[]);
|
| 900 |
1025 |
|
|
| 901 |
1026 |
|
let dir = scratch("vocab-prefix-clash");
|
| 902 |
1027 |
|
write(&dir, "css/styles.css", ".mo-card { box-shadow: none; }\n");
|
| 903 |
|
- |
assert!(std::panic::catch_unwind(|| check_vocabulary(&dir, &opts, &[], &[])).is_err());
|
|
1028 |
+ |
assert!(std::panic::catch_unwind(|| check_vocabulary(&dir, &opts, &[], &[], &[])).is_err());
|
| 904 |
1029 |
|
}
|
| 905 |
1030 |
|
|
| 906 |
1031 |
|
#[test]
|
| 913 |
1038 |
|
"css/styles.css",
|
| 914 |
1039 |
|
".badge { padding: 2px; border-radius: 3px; font-weight: 600; }\n",
|
| 915 |
1040 |
|
);
|
| 916 |
|
- |
check_vocabulary(&dir, &Emit::default(), &[], &[]);
|
|
1041 |
+ |
check_vocabulary(&dir, &Emit::default(), &[], &[], &[]);
|
| 917 |
1042 |
|
}
|
| 918 |
1043 |
|
|
| 919 |
1044 |
|
#[test]
|
| 920 |
1045 |
|
fn a_reviewed_pair_passes_and_stops_passing_when_it_stops_colliding() {
|
| 921 |
1046 |
|
let dir = scratch("vocab-allowed");
|
| 922 |
1047 |
|
write(&dir, "css/styles.css", ".card { box-shadow: none; }\n");
|
| 923 |
|
- |
check_vocabulary(&dir, &Emit::default(), &[], &[("card", "box-shadow")]);
|
|
1048 |
+ |
check_vocabulary(&dir, &Emit::default(), &[], &[("card", "box-shadow")], &[]);
|
| 924 |
1049 |
|
|
| 925 |
1050 |
|
// The same licence against a sheet that no longer collides has to fail,
|
| 926 |
1051 |
|
// or the list only ever grows.
|
| 927 |
1052 |
|
let dir = scratch("vocab-allowed-stale");
|
| 928 |
1053 |
|
write(&dir, "css/styles.css", ".card { padding: 2px; }\n");
|
| 929 |
1054 |
|
let err = std::panic::catch_unwind(|| {
|
| 930 |
|
- |
check_vocabulary(&dir, &Emit::default(), &[], &[("card", "box-shadow")]);
|
|
1055 |
+ |
check_vocabulary(&dir, &Emit::default(), &[], &[("card", "box-shadow")], &[]);
|
|
1056 |
+ |
})
|
|
1057 |
+ |
.unwrap_err();
|
|
1058 |
+ |
let msg = err
|
|
1059 |
+ |
.downcast_ref::<String>()
|
|
1060 |
+ |
.expect("panic payload is a String");
|
|
1061 |
+ |
assert!(msg.contains("no longer collides"), "got: {msg}");
|
|
1062 |
+ |
}
|
|
1063 |
+ |
|
|
1064 |
+ |
#[test]
|
|
1065 |
+ |
fn an_element_rule_clobbering_a_generated_class_fails_and_names_all_three() {
|
|
1066 |
+ |
let dir = scratch("vocab-element");
|
|
1067 |
+ |
// The defect that shipped for months: no class in the selector, so the
|
|
1068 |
+ |
// class pass sees nothing, and every described act in the app takes the
|
|
1069 |
+ |
// app's bevel instead of the design system's.
|
|
1070 |
+ |
write(&dir, "css/styles.css", "select { box-shadow: none; }\n");
|
|
1071 |
+ |
let err =
|
|
1072 |
+ |
std::panic::catch_unwind(|| check_vocabulary(&dir, &Emit::default(), &[], &[], &[]))
|
|
1073 |
+ |
.unwrap_err();
|
|
1074 |
+ |
let msg = err
|
|
1075 |
+ |
.downcast_ref::<String>()
|
|
1076 |
+ |
.expect("panic payload is a String");
|
|
1077 |
+ |
assert!(msg.contains("select {"), "got: {msg}");
|
|
1078 |
+ |
assert!(msg.contains(".field"), "got: {msg}");
|
|
1079 |
+ |
assert!(msg.contains("box-shadow"), "got: {msg}");
|
|
1080 |
+ |
assert!(msg.contains("css/styles.css"), "got: {msg}");
|
|
1081 |
+ |
}
|
|
1082 |
+ |
|
|
1083 |
+ |
#[test]
|
|
1084 |
+ |
fn a_handoff_on_the_class_is_the_remedy_and_reads_as_one() {
|
|
1085 |
+ |
let dir = scratch("vocab-element-handoff");
|
|
1086 |
+ |
// What a consumer writes instead of an exception: the element rule
|
|
1087 |
+ |
// stays, and a later layer gives the property back on the class. The
|
|
1088 |
+ |
// check has to read that as settled or the remedy fails the build it
|
|
1089 |
+ |
// was written to fix.
|
|
1090 |
+ |
write(
|
|
1091 |
+ |
&dir,
|
|
1092 |
+ |
"css/styles.css",
|
|
1093 |
+ |
"select { box-shadow: none; }\n.field { box-shadow: revert-layer; }\n",
|
|
1094 |
+ |
);
|
|
1095 |
+ |
check_vocabulary(&dir, &Emit::default(), &[], &[], &[]);
|
|
1096 |
+ |
}
|
|
1097 |
+ |
|
|
1098 |
+ |
#[test]
|
|
1099 |
+ |
fn a_property_the_app_states_on_the_class_is_not_the_element_rules_doing() {
|
|
1100 |
+ |
let dir = scratch("vocab-element-spoken-for");
|
|
1101 |
+ |
// Within the app's own sheet the class rule outranks the bare element
|
|
1102 |
+ |
// rule, so what reaches the design system is `.button`, not `button`.
|
|
1103 |
+ |
// The class pass has that pair -- here as a reviewed one -- and
|
|
1104 |
+ |
// reporting it twice would ask for two remedies for one collision.
|
|
1105 |
+ |
write(
|
|
1106 |
+ |
&dir,
|
|
1107 |
+ |
"css/styles.css",
|
|
1108 |
+ |
"select { box-shadow: none; }\n.field { box-shadow: none; }\n",
|
|
1109 |
+ |
);
|
|
1110 |
+ |
check_vocabulary(&dir, &Emit::default(), &[], &[("field", "box-shadow")], &[]);
|
|
1111 |
+ |
}
|
|
1112 |
+ |
|
|
1113 |
+ |
#[test]
|
|
1114 |
+ |
fn a_handoff_that_loses_to_the_rule_it_remedies_is_not_a_remedy() {
|
|
1115 |
+ |
let dir = scratch("vocab-element-weak-handoff");
|
|
1116 |
+ |
// The shape that reads as fixed and is not: both rules are the app's
|
|
1117 |
+ |
// and both are in the same layer, so the state on the element rule
|
|
1118 |
+ |
// decides, and the described field keeps the app's sunken fill.
|
|
1119 |
+ |
write(
|
|
1120 |
+ |
&dir,
|
|
1121 |
+ |
"css/styles.css",
|
|
1122 |
+ |
"select:focus { box-shadow: none; }\n.field { box-shadow: revert-layer; }\n",
|
|
1123 |
+ |
);
|
|
1124 |
+ |
let err =
|
|
1125 |
+ |
std::panic::catch_unwind(|| check_vocabulary(&dir, &Emit::default(), &[], &[], &[]))
|
|
1126 |
+ |
.unwrap_err();
|
|
1127 |
+ |
let msg = err
|
|
1128 |
+ |
.downcast_ref::<String>()
|
|
1129 |
+ |
.expect("panic payload is a String");
|
|
1130 |
+ |
assert!(msg.contains(".field"), "got: {msg}");
|
|
1131 |
+ |
|
|
1132 |
+ |
// Written to win, it is.
|
|
1133 |
+ |
let dir = scratch("vocab-element-strong-handoff");
|
|
1134 |
+ |
write(
|
|
1135 |
+ |
&dir,
|
|
1136 |
+ |
"css/styles.css",
|
|
1137 |
+ |
"select:focus { box-shadow: none; }\nselect.field { box-shadow: revert-layer; }\n",
|
|
1138 |
+ |
);
|
|
1139 |
+ |
check_vocabulary(&dir, &Emit::default(), &[], &[], &[]);
|
|
1140 |
+ |
}
|
|
1141 |
+ |
|
|
1142 |
+ |
#[test]
|
|
1143 |
+ |
fn a_scoped_rule_is_not_read_as_an_element_rule() {
|
|
1144 |
+ |
let dir = scratch("vocab-element-scoped");
|
|
1145 |
+ |
// It reaches the buttons inside one region rather than every button, so
|
|
1146 |
+ |
// whether it lands on a described act depends on where that act
|
|
1147 |
+ |
// renders. Failing the build on a guess is the worse error.
|
|
1148 |
+ |
write(
|
|
1149 |
+ |
&dir,
|
|
1150 |
+ |
"css/styles.css",
|
|
1151 |
+ |
".wizard select { box-shadow: none; }\n",
|
|
1152 |
+ |
);
|
|
1153 |
+ |
check_vocabulary(&dir, &Emit::default(), &[], &[], &[]);
|
|
1154 |
+ |
}
|
|
1155 |
+ |
|
|
1156 |
+ |
#[test]
|
|
1157 |
+ |
fn an_element_the_design_system_never_renders_onto_is_left_alone() {
|
|
1158 |
+ |
let dir = scratch("vocab-element-unpaired");
|
|
1159 |
+ |
// `.card` is a container: no generated class sits on a `<footer>`, so
|
|
1160 |
+ |
// there is nothing for this rule to take.
|
|
1161 |
+ |
write(&dir, "css/styles.css", "footer { box-shadow: none; }\n");
|
|
1162 |
+ |
check_vocabulary(&dir, &Emit::default(), &[], &[], &[]);
|
|
1163 |
+ |
}
|
|
1164 |
+ |
|
|
1165 |
+ |
#[test]
|
|
1166 |
+ |
fn a_reviewed_element_pairing_passes_and_stops_passing_when_it_stops_colliding() {
|
|
1167 |
+ |
let dir = scratch("vocab-element-allowed");
|
|
1168 |
+ |
write(&dir, "css/styles.css", "select { box-shadow: none; }\n");
|
|
1169 |
+ |
check_vocabulary(
|
|
1170 |
+ |
&dir,
|
|
1171 |
+ |
&Emit::default(),
|
|
1172 |
+ |
&[],
|
|
1173 |
+ |
&[],
|
|
1174 |
+ |
&[("select", "field", "box-shadow")],
|
|
1175 |
+ |
);
|
|
1176 |
+ |
|
|
1177 |
+ |
// And the same licence against a sheet that no longer collides fails,
|
|
1178 |
+ |
// for the reason the class list's does.
|
|
1179 |
+ |
let dir = scratch("vocab-element-allowed-stale");
|
|
1180 |
+ |
write(&dir, "css/styles.css", "select { padding: 2px; }\n");
|
|
1181 |
+ |
let err = std::panic::catch_unwind(|| {
|
|
1182 |
+ |
check_vocabulary(
|
|
1183 |
+ |
&dir,
|
|
1184 |
+ |
&Emit::default(),
|
|
1185 |
+ |
&[],
|
|
1186 |
+ |
&[],
|
|
1187 |
+ |
&[("select", "field", "box-shadow")],
|
|
1188 |
+ |
);
|
| 931 |
1189 |
|
})
|
| 932 |
1190 |
|
.unwrap_err();
|
| 933 |
1191 |
|
let msg = err
|