max / makeover-webview
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
3 files changed,
+46 insertions,
-2 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "makeover-webview" | |
| 3 | - | version = "0.74.0" | |
| 3 | + | version = "0.74.1" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | # One copy of this renderer per dependency graph, enforced by cargo rather than | |
| 6 | 6 | # by remembering. Two versions means the generated stylesheet and the emitted |
| @@ -679,6 +679,10 @@ | |||
| 679 | 679 | /// The options of a select: the unanswered instruction, an unmatched current | |
| 680 | 680 | /// value carried as its own, then the options themselves. | |
| 681 | 681 | /// | |
| 682 | + | /// An option is marked either by [`Choice::chosen`] or by carrying the field's | |
| 683 | + | /// current value; the stray-option and placeholder paths below key on the value | |
| 684 | + | /// alone, so a list that marks itself has an empty value and reaches neither. | |
| 685 | + | /// | |
| 682 | 686 | /// A select handed a value no option carries renders with nothing selected, the | |
| 683 | 687 | /// browser falls back to the first option, and the next save writes a value | |
| 684 | 688 | /// nobody chose. goingson hit exactly that with a backup-retention default of | |
| @@ -720,7 +724,18 @@ | |||
| 720 | 724 | out.push_str("<option value=\""); | |
| 721 | 725 | escape_into(opt.value, out); | |
| 722 | 726 | out.push('"'); | |
| 723 | - | if opt.value == value { | |
| 727 | + | // Two ways an option is the marked one, and a description uses one of | |
| 728 | + | // them: the option says so itself, or the field's value names it. See | |
| 729 | + | // [`makeover_layout::Choice::chosen`] for why both exist and why this | |
| 730 | + | // crate cannot refuse the pair -- a caller that sets both gets both | |
| 731 | + | // marked, and quasi-declare is where that is caught. | |
| 732 | + | // | |
| 733 | + | // A `placeholder` is unaffected and still rides on an empty value: it | |
| 734 | + | // is emitted `selected` to show the unanswered state, and a list whose | |
| 735 | + | // own option is chosen leaves two options selected, which HTML resolves | |
| 736 | + | // to the last one in tree order. That is the chosen option, since the | |
| 737 | + | // placeholder is emitted first. | |
| 738 | + | if opt.chosen || opt.value == value { | |
| 724 | 739 | out.push_str(" selected"); | |
| 725 | 740 | } | |
| 726 | 741 | // `disabled` is what the browser reads, and it says nothing about why. |
| @@ -294,6 +294,35 @@ | |||
| 294 | 294 | assert!(html.contains("<option value=\"10\" selected"), "{html}"); | |
| 295 | 295 | } | |
| 296 | 296 | ||
| 297 | + | #[test] | |
| 298 | + | fn a_select_marks_the_option_that_says_it_is_chosen() { | |
| 299 | + | // The option list knows per row, so the field carries no value at all. | |
| 300 | + | // Comparing a value against each option is the other spelling and this | |
| 301 | + | // test is the one that says they are separate paths. | |
| 302 | + | let options = [ | |
| 303 | + | Choice::plain("dark"), | |
| 304 | + | Choice::plain("light").chosen(), | |
| 305 | + | Choice::plain("paper"), | |
| 306 | + | ]; | |
| 307 | + | let f = Field::select("title", "Title", &options); | |
| 308 | + | let html = field_html(&f, &Filling::of(Value::Text("")), &Emit::default()); | |
| 309 | + | assert!(html.contains("<option value=\"light\" selected"), "{html}"); | |
| 310 | + | assert!(html.contains("<option value=\"dark\">"), "{html}"); | |
| 311 | + | assert!(html.contains("<option value=\"paper\">"), "{html}"); | |
| 312 | + | // The empty value reaches neither the stray-option path nor a placeholder. | |
| 313 | + | assert!(!html.contains("data-unmatched"), "{html}"); | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | #[test] | |
| 317 | + | fn a_select_whose_options_mark_none_marks_none() { | |
| 318 | + | // The other half of the branch, and the shape a residual holds: the same | |
| 319 | + | // compiled row body with the guard not passing. | |
| 320 | + | let options = [Choice::plain("dark"), Choice::plain("light")]; | |
| 321 | + | let f = Field::select("title", "Title", &options); | |
| 322 | + | let html = field_html(&f, &Filling::of(Value::Text("")), &Emit::default()); | |
| 323 | + | assert!(!html.contains("selected"), "{html}"); | |
| 324 | + | } | |
| 325 | + | ||
| 297 | 326 | #[test] | |
| 298 | 327 | fn a_select_with_no_options_emits_an_empty_select() { | |
| 299 | 328 | // The description says a select with no options is sayable, because an |