use super::*;
use makeover_layout::{Accepted, Curve, Family};
fn field(kind: FieldKind) -> Field<'static> {
Field::new(kind, "title", "Title")
}
#[test]
fn a_value_cannot_break_out_of_the_attribute_it_sits_in() {
let filling = Filling::of(Value::Text("x\" onfocus=alert(1) autofocus=\""));
let html = field_html(&field(FieldKind::Text), &filling, &Emit::default());
assert!(!html.contains("\" onfocus"), "{html}");
assert!(
html.contains("value=\"x" onfocus=alert(1) autofocus="\""),
"{html}"
);
}
#[test]
fn a_host_can_write_its_own_attributes_onto_the_control() {
let mut filling = Filling::of(Value::Text("ru"));
filling.control_attrs = Some(Markup(
r#"role="combobox" aria-expanded="false" aria-controls="title-suggestions""#,
));
let html = field_html(&field(FieldKind::Text), &filling, &Emit::default());
assert!(html.contains(r#"role="combobox""#), "{html}");
assert!(
html.contains(r#"aria-controls="title-suggestions""#),
"{html}"
);
let id = html.find(r#"id="title""#).expect("id");
let role = html.find(r#"role="combobox""#).expect("role");
assert!(id < role, "{html}");
}
#[test]
fn a_radio_group_drops_control_attributes() {
let mut f = field(FieldKind::Radio);
let options = [Choice::new("a", "A")];
f.options = &options;
let filling = Filling {
control_attrs: Some(Markup(r#"data-host="1""#)),
..Filling::default()
};
let html = field_html(&f, &filling, &Emit::default());
assert!(!html.contains("data-host"), "{html}");
}
#[test]
fn a_label_cannot_open_a_tag() {
let mut f = field(FieldKind::Text);
f.label = "<script>alert(1)</script>";
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(!html.contains("<script>"), "{html}");
assert!(html.contains("<script>"), "{html}");
}
#[test]
fn every_escaped_sink_is_covered_by_the_one_escaper() {
assert_eq!(escape("&<>\"'"), "&<>"'");
assert!(escape("\"").contains("""));
}
#[test]
fn the_streaming_escaper_appends_what_the_returning_one_returns() {
for text in [
"",
"plain",
"&<>\"'",
"&&&",
"a & b",
"trailing&",
"&leading",
"é世 & <b>naïve</b> \u{1f600}",
] {
let mut out = String::from("kept: ");
escape_into(text, &mut out);
assert_eq!(out, format!("kept: {}", escape(text)), "{text:?}");
}
}
#[test]
fn a_streamed_field_is_the_field_the_other_form_returns() {
let kinds = [
FieldKind::Text,
FieldKind::Secret,
FieldKind::Number,
FieldKind::Checkbox,
FieldKind::Radio,
FieldKind::Select,
FieldKind::Textarea,
FieldKind::File,
FieldKind::Hidden,
];
let choices = [Choice::plain("one"), Choice::plain("two")];
let opts = Emit {
class_prefix: "mk-",
..Emit::default()
};
for kind in kinds {
let described = Field {
hint: Some("a hint"),
error: Some("wrong <here>"),
placeholder: Some("x\" y"),
options: &choices,
required: true,
max_length: Some(40),
min: Some("1"),
max: Some("9"),
extended: true,
..Field::new(kind, "the & name", "The <label>")
};
let filling = Filling {
value: Value::Text("one"),
trailing: Some(Markup("<i>t</i>")),
control_attrs: Some(Markup(r#"data-host="1""#)),
id_prefix: Some("modal"),
};
let mut streamed = String::new();
field_html_into(&described, &filling, &opts, &mut streamed);
assert_eq!(
streamed,
field_html(&described, &filling, &opts),
"{kind:?}"
);
let plain = Field::new(kind, "name", "Label");
let mut streamed = String::new();
field_html_into(&plain, &Filling::default(), &opts, &mut streamed);
assert_eq!(
streamed,
field_html(&plain, &Filling::default(), &opts),
"{kind:?}"
);
}
}
#[test]
fn markup_is_the_only_way_past_the_escaping() {
let filling = Filling {
trailing: Some(Markup("<div class=\"recurrence-config\"></div>")),
..Filling::default()
};
let html = field_html(&field(FieldKind::Text), &filling, &Emit::default());
assert!(
html.contains("<div class=\"recurrence-config\"></div>"),
"{html}"
);
}
#[test]
fn an_invalid_field_carries_the_attribute_its_own_stylesheet_keys_on() {
let mut f = field(FieldKind::Text);
f.error = Some("Required");
let opts = Emit::default();
let html = field_html(&f, &Filling::default(), &opts);
assert!(html.contains("aria-invalid=\"true\""), "{html}");
assert!(crate::stylesheet(&opts).contains("[aria-invalid=\"true\"]"));
assert!(html.contains("has-error"), "{html}");
}
#[test]
fn a_valid_field_claims_nothing_about_being_invalid() {
let html = field_html(
&field(FieldKind::Text),
&Filling::default(),
&Emit::default(),
);
assert!(!html.contains("aria-invalid"), "{html}");
assert!(!html.contains("has-error"), "{html}");
}
#[test]
fn a_note_sits_between_the_hint_and_the_error_and_carries_its_tone() {
let mut f = field(FieldKind::Text);
f.hint = Some("Keep it short");
f.note = Some((Tone::Warning, "Re-encoding drops embedded BWF"));
f.error = Some("Required");
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(
html.contains(r#"aria-describedby="title-hint title-note title-error""#),
"{html}"
);
assert!(
html.contains(r#"id="title-note" role="alert" data-tone="warning""#),
"{html}"
);
let hint = html.find("title-hint").unwrap();
let note = html.rfind("title-note").unwrap();
let err = html.rfind("title-error").unwrap();
assert!(hint < note && note < err, "{html}");
}
#[test]
fn a_quiet_note_is_polite_and_wears_no_tone_attribute() {
let mut f = field(FieldKind::Text);
f.note = Some((Tone::Info, "This is what that setting implies"));
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains(r#"role="status" data-tone="info""#), "{html}");
f.note = Some((Tone::Neutral, "An ordinary fact"));
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains(r#"id="title-note" role="status">"#), "{html}");
assert!(!html.contains("data-tone"), "{html}");
}
#[test]
fn a_note_does_not_mark_the_group_invalid() {
let mut f = field(FieldKind::Text);
f.note = Some((Tone::Danger, "This cannot be undone"));
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(!html.contains("has-error"), "{html}");
assert!(!html.contains(r#"aria-invalid="true""#), "{html}");
}
#[test]
fn the_hint_survives_an_error_arriving() {
let mut f = field(FieldKind::Text);
f.hint = Some("Keep it short");
f.error = Some("Required");
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(
html.contains("aria-describedby=\"title-hint title-error\""),
"{html}"
);
}
#[test]
fn a_secret_never_carries_its_value_into_the_markup() {
let filling = Filling::of(Value::Text("hunter2"));
let html = field_html(&field(FieldKind::Secret), &filling, &Emit::default());
assert!(!html.contains("hunter2"), "{html}");
assert!(html.contains("type=\"password\""), "{html}");
}
#[test]
fn a_hidden_field_is_the_input_and_nothing_else() {
let filling = Filling::of(Value::Text("42"));
let html = field_html(&field(FieldKind::Hidden), &filling, &Emit::default());
assert_eq!(html, "<input type=\"hidden\" name=\"title\" value=\"42\">");
}
#[test]
fn a_checkbox_labels_itself_and_takes_no_separate_label() {
let html = field_html(
&field(FieldKind::Checkbox),
&Filling::of(Value::On(true)),
&Emit::default(),
);
assert!(!html.contains("form-label"), "{html}");
assert!(html.contains("checked"), "{html}");
assert!(html.contains("<span>Title</span>"), "{html}");
}
#[test]
fn a_select_keeps_a_value_no_option_carries() {
let options = [Choice::plain("1"), Choice::plain("3"), Choice::plain("7")];
let f = Field::select("title", "Title", &options);
let html = field_html(&f, &Filling::of(Value::Text("10")), &Emit::default());
assert!(html.contains("data-unmatched=\"true\""), "{html}");
assert!(html.contains("<option value=\"10\" selected"), "{html}");
}
#[test]
fn a_select_with_no_options_emits_an_empty_select() {
let f = Field::select("title", "Title", &[]);
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains("<select"), "{html}");
assert!(!html.contains("<option"), "{html}");
}
#[test]
fn an_unanswered_select_shows_its_ghost_text_and_cannot_be_chosen_back() {
let options = [Choice::new("sp404", "SP-404")];
let f = Field {
placeholder: Some("Select device..."),
..Field::select("device", "Conform for device", &options)
};
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(
html.contains("<option value=\"\" disabled selected>Select device...</option>"),
"{html}"
);
assert!(
html.find("Select device...") < html.find("SP-404"),
"{html}"
);
}
#[test]
fn an_answered_select_drops_the_ghost_text() {
let options = [Choice::new("sp404", "SP-404")];
let f = Field {
placeholder: Some("Select device..."),
..Field::select("device", "Conform for device", &options)
};
let html = field_html(&f, &Filling::of(Value::Text("sp404")), &Emit::default());
assert!(!html.contains("Select device..."), "{html}");
}
#[test]
fn a_wrong_answer_is_kept_and_is_not_the_ghost_text() {
let options = [Choice::plain("1"), Choice::plain("7")];
let f = Field {
placeholder: Some("Pick one"),
..Field::select("retention", "Keep backups for", &options)
};
let html = field_html(&f, &Filling::of(Value::Text("10")), &Emit::default());
assert!(html.contains("data-unmatched=\"true\""), "{html}");
assert!(!html.contains("Pick one"), "{html}");
}
#[test]
fn a_range_is_a_range_input_and_carries_its_extent() {
let f = Field {
curve: Curve::Linear { step: Some("0.01") },
..Field::range("review", "Review above", "0", "1")
};
let html = field_html(&f, &Filling::of(Value::Text("0.72")), &Emit::default());
assert!(html.contains("type=\"range\""), "{html}");
assert!(html.contains("min=\"0\""), "{html}");
assert!(html.contains("max=\"1\""), "{html}");
assert!(html.contains("step=\"0.01\""), "{html}");
}
#[test]
fn a_range_reads_its_granularity_off_the_curve_and_not_off_field_step() {
let f = Field {
step: Some("99"),
..Field::range("review", "Review above", "0", "1")
};
let html = field_html(&f, &Filling::of(Value::Text("0.5")), &Emit::default());
assert!(!html.contains("step="), "{html}");
}
#[test]
fn a_unit_is_adjacent_text_and_the_control_points_at_it() {
let f = Field {
unit: Some("dBFS"),
..Field::range("threshold", "Threshold", "-96", "-20")
};
let html = field_html(&f, &Filling::of(Value::Text("-40")), &Emit::default());
assert!(html.contains(r#"id="threshold-unit""#), "{html}");
assert!(html.contains(">dBFS</span>"), "{html}");
assert!(
html.contains(r#"aria-describedby="threshold-unit""#),
"{html}"
);
assert!(html.contains(">Threshold</label>"), "{html}");
}
#[test]
fn a_unit_takes_its_place_between_the_hint_and_the_error() {
let f = Field {
unit: Some("ms"),
hint: Some("How long the fade runs."),
error: Some("Too long."),
..Field::new(FieldKind::Number, "fade", "Fade")
};
let html = field_html(&f, &Filling::of(Value::Text("50")), &Emit::default());
assert!(
html.contains(r#"aria-describedby="fade-hint fade-unit fade-error""#),
"{html}"
);
}
#[test]
fn a_unit_on_a_kind_that_is_not_a_quantity_is_ignored() {
let f = Field {
unit: Some("s"),
..Field::new(FieldKind::Text, "name", "Name")
};
let html = field_html(&f, &Filling::of(Value::Text("kick")), &Emit::default());
assert!(!html.contains("name-unit"), "{html}");
assert!(!html.contains("aria-describedby"), "{html}");
}
#[test]
fn a_unit_cannot_break_out_of_the_span_it_sits_in() {
let f = Field {
unit: Some("</span><script>"),
..Field::new(FieldKind::Number, "n", "N")
};
let html = field_html(&f, &Filling::of(Value::Text("1")), &Emit::default());
assert!(!html.contains("<script>"), "{html}");
assert!(html.contains("<script>"), "{html}");
}
#[test]
fn a_constant_ratio_curve_is_answered_with_a_linear_track() {
let f = Field {
curve: Curve::Logarithmic {
step: Some("0.001"),
},
..Field::range("attack", "Attack", "0.001", "5")
};
let html = field_html(&f, &Filling::of(Value::Text("0.005")), &Emit::default());
assert!(html.contains("type=\"range\""), "{html}");
assert!(html.contains("min=\"0.001\""), "{html}");
assert!(html.contains("max=\"5\""), "{html}");
assert!(html.contains("step=\"0.001\""), "{html}");
}
#[test]
fn a_number_with_bounds_is_still_typed_into() {
let f = Field {
min: Some("1"),
..Field::new(FieldKind::Number, "minutes", "Minutes")
};
let html = field_html(&f, &Filling::of(Value::Text("30")), &Emit::default());
assert!(html.contains("type=\"number\""), "{html}");
assert!(!html.contains("type=\"range\""), "{html}");
assert!(!html.contains("step="), "{html}");
}
#[test]
fn an_unavailable_option_is_disabled_and_says_why() {
let options = [
Choice::new("chromatic", "Chromatic"),
Choice::new("multi", "Multi-sample").unless("Drop a second sample."),
];
let f = Field::radio("mode", "Mode", &options);
let html = field_html(&f, &Filling::of(Value::Text("chromatic")), &Emit::default());
assert!(html.contains(" disabled"), "{html}");
assert!(html.contains("Drop a second sample."), "{html}");
assert!(html.contains("value=\"multi\""), "{html}");
assert!(html.contains("form-option-reason"), "{html}");
}
#[test]
fn an_unavailable_select_option_carries_its_reason_in_its_text() {
let options = [Choice::new("multi", "Multi-sample").unless("Drop a second sample.")];
let f = Field::select("mode", "Mode", &options);
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(
html.contains(">Multi-sample: Drop a second sample.</option>"),
"{html}"
);
assert!(html.contains("disabled"), "{html}");
}
#[test]
fn an_option_can_say_what_picking_it_means() {
let options = [
Choice::new("16", "Basic").detailing("$16/mo. Fits text, blogs, newsletters."),
Choice::new("24", "Small Files"),
];
let f = Field::radio("tier", "Content tier", &options);
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains("form-option-detail"), "{html}");
assert!(
html.contains(">$16/mo. Fits text, blogs, newsletters.</span>"),
"{html}"
);
assert_eq!(html.matches("form-option-detail").count(), 1, "{html}");
assert!(
option_detail_rules(&Emit::default()).contains("var(--content-muted)"),
"the line orients the label rather than competing with it"
);
}
#[test]
fn an_option_reads_what_it_is_before_why_it_cannot_be_picked() {
let options = [Choice::new("24", "Small Files")
.detailing("$24/mo. Fits audio, plugins, binaries.")
.unless("Sold out while the founder window is open.")];
let f = Field::radio("tier", "Content tier", &options);
let html = field_html(&f, &Filling::default(), &Emit::default());
let detail = html.find("form-option-detail").expect("the detail");
let reason = html.find("form-option-reason").expect("the reason");
assert!(detail < reason, "{html}");
assert!(html.contains(" disabled"), "{html}");
let f = Field::select("tier", "Content tier", &options);
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(
html.contains(concat!(
">Small Files: $24/mo. Fits audio, plugins, binaries.",
": Sold out while the founder window is open.</option>"
)),
"{html}"
);
}
#[test]
fn a_radio_group_is_named_by_its_label_instead_of_pointing_at_it() {
let options = [Choice::plain("copy"), Choice::plain("reference")];
let f = Field::radio("storage", "Storage style", &options);
let html = field_html(&f, &Filling::of(Value::Text("copy")), &Emit::default());
assert!(html.contains("id=\"storage-label\""), "{html}");
assert!(!html.contains("for=\"storage\""), "{html}");
assert!(html.contains("role=\"radiogroup\""), "{html}");
assert!(html.contains("aria-labelledby=\"storage-label\""), "{html}");
}
#[test]
fn an_interval_is_one_labelled_group_holding_both_ends() {
let f = Field::interval("min_price", "max_price", "Price");
let html = field_html(
&f,
&Filling::of(Value::Between {
lower: "5",
upper: "40",
}),
&Emit::default(),
);
assert!(html.contains("role=\"group\""), "{html}");
assert!(
html.contains("aria-labelledby=\"min_price-label\""),
"{html}"
);
assert!(html.contains("id=\"min_price-label\""), "{html}");
assert!(!html.contains("for=\"min_price\""), "{html}");
assert!(html.contains("name=\"min_price\""), "{html}");
assert!(html.contains("name=\"max_price\""), "{html}");
assert!(html.contains("value=\"5\""), "{html}");
assert!(html.contains("value=\"40\""), "{html}");
assert_eq!(html.matches("type=\"number\"").count(), 2, "{html}");
}
#[test]
fn both_ends_of_an_interval_take_the_whole_extent() {
let f = Field {
min: Some("0"),
max: Some("300"),
step: Some("1"),
..Field::interval("bpm_min", "bpm_max", "BPM")
};
let html = field_html(&f, &Filling::default(), &Emit::default());
assert_eq!(html.matches("min=\"0\"").count(), 2, "{html}");
assert_eq!(html.matches("max=\"300\"").count(), 2, "{html}");
assert_eq!(html.matches("step=\"1\"").count(), 2, "{html}");
assert_eq!(html.matches("value=\"\"").count(), 2, "{html}");
}
#[test]
fn an_interval_carries_the_fault_on_the_group_and_not_on_one_end() {
let f = Field {
error: Some("The high end is below the low one."),
hint: Some("Leave an end empty for no bound."),
..Field::interval("bpm_min", "bpm_max", "BPM")
};
let html = field_html(&f, &Filling::default(), &Emit::default());
assert_eq!(html.matches("aria-invalid=\"true\"").count(), 1, "{html}");
let group = html.find("role=\"group\"").expect("group");
let invalid = html.find("aria-invalid").expect("invalid");
let first_input = html.find("<input").expect("input");
assert!(invalid > group && invalid < first_input, "{html}");
assert!(
html.contains("aria-describedby=\"bpm_min-hint bpm_min-error\""),
"{html}"
);
}
#[test]
fn an_interval_with_one_end_named_draws_one_box() {
let f = Field::new(FieldKind::Interval, "bpm_min", "BPM");
let html = field_html(&f, &Filling::default(), &Emit::default());
assert_eq!(html.matches("<input").count(), 1, "{html}");
assert!(html.contains("name=\"bpm_min\""), "{html}");
}
#[test]
fn every_option_shares_the_name_and_only_the_current_one_is_checked() {
let options = [
Choice::plain("copy"),
Choice::plain("reference"),
Choice::plain("link"),
];
let f = Field::radio("storage", "Storage style", &options);
let html = field_html(&f, &Filling::of(Value::Text("reference")), &Emit::default());
assert_eq!(html.matches("name=\"storage\"").count(), 3, "{html}");
assert_eq!(html.matches(" checked").count(), 1, "{html}");
assert!(
html.contains("value=\"reference\" checked"),
"the checked one is the one held: {html}"
);
for index in 0..3 {
assert!(html.contains(&format!("id=\"storage-{index}\"")), "{html}");
}
}
#[test]
fn a_radio_group_carries_the_error_rather_than_any_one_option() {
let options = [Choice::plain("copy"), Choice::plain("reference")];
let f = Field {
error: Some("Pick one."),
hint: Some("Cannot be changed later."),
..Field::radio("storage", "Storage style", &options)
};
let html = field_html(&f, &Filling::default(), &Emit::default());
assert_eq!(html.matches("aria-invalid=\"true\"").count(), 1, "{html}");
assert!(
html.contains("aria-describedby=\"storage-hint storage-error\""),
"{html}"
);
let group = html.find("role=\"radiogroup\"").expect("group");
let first = html.find("type=\"radio\"").expect("an option");
assert!(group < first, "{html}");
}
#[test]
fn a_compulsory_radio_group_marks_every_option() {
let options = [Choice::plain("copy"), Choice::plain("reference")];
let f = Field {
required: true,
..Field::radio("storage", "Storage style", &options)
};
let html = field_html(&f, &Filling::default(), &Emit::default());
assert_eq!(html.matches(" required").count(), 2, "{html}");
}
#[test]
fn a_radio_option_cannot_break_out_of_its_attribute() {
let hostile = [Choice::new(
"x\" onclick=alert(1) data-x=\"",
"<script>alert(1)</script>",
)];
let f = Field::radio("storage", "Storage style", &hostile);
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains("value=\"x" onclick=alert(1)"), "{html}");
assert!(!html.contains("<script>"), "{html}");
assert!(html.contains("id=\"storage-0\""), "{html}");
}
#[test]
fn a_radio_group_with_no_options_emits_an_empty_group() {
let f = Field::radio("storage", "Storage style", &[]);
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains("role=\"radiogroup\""), "{html}");
assert!(!html.contains("type=\"radio\""), "{html}");
}
#[test]
fn a_placeholder_comes_off_the_description_and_is_escaped() {
let f = Field {
placeholder: Some("x\" onfocus=alert(1) autofocus=\""),
..field(FieldKind::Text)
};
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains("placeholder=\""), "{html}");
assert!(!html.contains("\" onfocus"), "{html}");
}
#[test]
fn a_select_marks_the_option_that_matches() {
let options = [Choice::plain("1"), Choice::plain("3")];
let f = Field::select("title", "Title", &options);
let html = field_html(&f, &Filling::of(Value::Text("3")), &Emit::default());
assert!(
html.contains("<option value=\"3\" selected>3</option>"),
"{html}"
);
assert!(html.contains("<option value=\"1\">1</option>"), "{html}");
assert!(!html.contains("data-unmatched"), "{html}");
}
#[test]
fn a_textarea_carries_its_value_as_text_and_not_as_an_attribute() {
let filling = Filling::of(Value::Text("two\nlines"));
let html = field_html(&field(FieldKind::Textarea), &filling, &Emit::default());
assert!(html.contains(">two\nlines</textarea>"), "{html}");
}
#[test]
fn a_markdown_field_is_a_textarea_that_says_what_its_value_is() {
let filling = Filling::of(Value::Text("# Heading"));
let html = field_html(&field(FieldKind::Rich), &filling, &Emit::default());
assert!(html.contains("<textarea"), "{html}");
assert!(html.contains(r#"data-format="markdown""#), "{html}");
assert!(html.contains("># Heading</textarea>"), "{html}");
let plain = field_html(&field(FieldKind::Textarea), &filling, &Emit::default());
assert!(!plain.contains("data-format"), "{plain}");
assert!(!html.contains("<input"), "{html}");
}
#[test]
fn a_markdown_field_gets_the_preview_the_member_permits() {
let filling = Filling::of(Value::Text("# Heading"));
let html = field_html(&field(FieldKind::Rich), &filling, &Emit::default());
assert!(html.contains("data-editor-mode=\"write\""), "{html}");
assert!(html.contains("data-editor-mode=\"preview\""), "{html}");
assert!(html.contains("data-editor-preview"), "{html}");
assert!(
html.contains("class=\"segment chosen\" data-editor-mode=\"write\" aria-pressed=\"true\""),
"{html}"
);
assert!(
html.contains("data-editor-mode=\"preview\" aria-pressed=\"false\""),
"{html}"
);
assert!(html.contains("># Heading</textarea>"), "{html}");
}
#[test]
fn a_plain_textarea_gets_no_editor_chrome() {
let filling = Filling::of(Value::Text("plain"));
let html = field_html(&field(FieldKind::Textarea), &filling, &Emit::default());
assert!(!html.contains("data-editor-mode"), "{html}");
assert!(!html.contains("data-editor-preview"), "{html}");
assert!(!html.contains("segment"), "{html}");
}
#[test]
fn nothing_the_editor_emits_renders_the_value_as_markup() {
let filling = Filling::of(Value::Text("<img src=x onerror=alert(1)>"));
let html = field_html(&field(FieldKind::Rich), &filling, &Emit::default());
assert!(html.contains("data-editor-preview></div>"), "{html}");
assert!(!html.contains("<img"), "{html}");
assert!(
html.contains("<img src=x onerror=alert(1)>"),
"{html}"
);
}
#[test]
fn the_editor_rules_gate_on_the_attribute_and_on_a_binding() {
let css = editor_rules(&Emit::default());
for line in css.lines().filter(|line| line.contains('{')) {
assert!(line.contains("[data-format=\"markdown\"]"), "{line}");
}
assert!(
css.contains("[data-format=\"markdown\"] > .form-editor-modes {\n display: none;\n}")
);
assert!(css.contains(
"[data-format=\"markdown\"][data-ready] > .form-editor-modes {\n display: block;\n}"
));
assert!(css.contains(
"[data-ready][data-mode=\"preview\"] > .form-editor-preview {\n display: block;\n}"
));
assert!(css.contains("[data-ready][data-mode=\"preview\"] > .field {\n display: none;\n}"));
assert!(!css.contains("px"), "{css}");
assert!(!css.contains("rem"), "{css}");
}
#[test]
fn the_editor_chrome_is_prefixed_and_its_gate_is_not() {
let opts = Emit {
class_prefix: "mk-",
..Emit::default()
};
let html = field_html(&field(FieldKind::Rich), &Filling::default(), &opts);
assert!(html.contains("class=\"mk-form-editor-modes\""), "{html}");
assert!(html.contains("class=\"mk-form-editor-preview\""), "{html}");
assert!(html.contains("class=\"mk-segment chosen\""), "{html}");
assert!(html.contains("data-format=\"markdown\""), "{html}");
let css = editor_rules(&opts);
assert!(css.contains(".mk-form-editor-modes"), "{css}");
assert!(css.contains("[data-format=\"markdown\"]"), "{css}");
}
#[test]
fn the_editor_classes_are_in_the_vocabulary() {
let opts = Emit::default();
let names = crate::vocabulary::names(&opts);
for name in ["form-editor-modes", "form-editor-preview", "segment"] {
assert!(names.contains(name), "{name} is not in the vocabulary");
}
}
#[test]
fn the_class_prefix_reaches_the_markup_as_well_as_the_stylesheet() {
let opts = Emit {
class_prefix: "mk-",
..Emit::default()
};
let html = field_html(&field(FieldKind::Text), &Filling::default(), &opts);
assert!(html.contains("class=\"mk-form-group\""), "{html}");
assert!(html.contains("class=\"mk-field\""), "{html}");
}
#[test]
fn a_datetime_asking_for_an_instant_is_marked_for_the_script_that_converts_it() {
let mut f = field(FieldKind::DateTime);
f.as_instant = true;
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains("data-instant=\"true\""), "{html}");
assert!(html.contains("type=\"datetime-local\""), "{html}");
}
#[test]
fn only_a_datetime_can_name_a_moment_so_only_a_datetime_is_marked() {
for kind in [FieldKind::Date, FieldKind::Text, FieldKind::Number] {
let mut f = field(kind);
f.as_instant = true;
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(!html.contains("data-instant"), "{kind:?}: {html}");
}
}
#[test]
fn a_datetime_that_did_not_ask_carries_no_mark() {
let html = field_html(
&field(FieldKind::DateTime),
&Filling::default(),
&Emit::default(),
);
assert!(!html.contains("data-instant"), "{html}");
}
#[test]
fn an_extended_field_says_so_and_leaves_the_disclosure_to_the_form() {
let mut f = field(FieldKind::Text);
f.extended = true;
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains("data-extended=\"true\""), "{html}");
}
#[test]
fn the_id_prefix_scopes_the_id_and_never_the_name() {
let mut f = field(FieldKind::Text);
f.hint = Some("Keep it short");
f.error = Some("Required");
let filling = Filling {
id_prefix: Some("form-modal-task-edit"),
..Filling::default()
};
let html = field_html(&f, &filling, &Emit::default());
assert!(
html.contains(r#"id="form-modal-task-edit-title""#),
"{html}"
);
assert!(html.contains(r#"name="title""#), "{html}");
assert!(
!html.contains(r#"name="form-modal-task-edit-title""#),
"{html}"
);
assert!(
html.contains(r#"for="form-modal-task-edit-title""#),
"{html}"
);
assert!(
html.contains(
r#"aria-describedby="form-modal-task-edit-title-hint form-modal-task-edit-title-error""#
),
"{html}"
);
assert!(
html.contains(r#"id="form-modal-task-edit-title-hint""#),
"{html}"
);
}
#[test]
fn a_hidden_field_submits_its_bare_name_under_a_prefix() {
let filling = Filling {
value: Value::Text("42"),
id_prefix: Some("scoped"),
..Filling::default()
};
let html = field_html(&field(FieldKind::Hidden), &filling, &Emit::default());
assert_eq!(html, r#"<input type="hidden" name="title" value="42">"#);
}
#[test]
fn a_constraint_becomes_the_browsers_own_attribute() {
let html = field_html(
&Field {
max_length: Some(100),
min: Some("1"),
max: Some("240"),
required: true,
..Field::new(FieldKind::Number, "minutes", "Minutes")
},
&Filling::default(),
&Emit::default(),
);
assert!(html.contains(r#"maxlength="100""#));
assert!(html.contains(r#"min="1""#));
assert!(html.contains(r#"max="240""#));
assert!(html.contains(" required"));
}
#[test]
fn a_bound_is_emitted_as_written_and_escaped_like_anything_else() {
let html = field_html(
&Field {
min: Some("2026-08-09T14:30"),
..Field::new(FieldKind::Text, "starts", "Starts")
},
&Filling::default(),
&Emit::default(),
);
assert!(html.contains(r#"min="2026-08-09T14:30""#));
}
#[test]
fn a_file_field_is_a_file_input() {
let html = field_html(
&Field::new(FieldKind::File, "attachment", "Attachment"),
&Filling::default(),
&Emit::default(),
);
assert!(html.contains(r#"type="file""#));
assert!(!html.contains("accept="));
assert!(!html.contains("multiple"));
assert!(!html.contains("value="));
}
#[test]
fn an_accept_list_is_comma_joined_in_the_attributes_own_format() {
const MIXED: &[Accepted<'_>] = &[
Accepted::Family(Family::Image),
Accepted::Type("text/csv"),
Accepted::Suffix(".tar.gz"),
];
let html = field_html(
&Field {
multiple: true,
..Field::upload("drop", "Drop files", MIXED)
},
&Filling::default(),
&Emit::default(),
);
assert!(
html.contains(r#"accept="image/*,text/csv,.tar.gz""#),
"{html}"
);
assert!(html.contains(" multiple"), "{html}");
}
#[test]
fn an_accept_entry_cannot_end_the_attribute_it_sits_in() {
const HOSTILE: &[Accepted<'_>] = &[Accepted::Type(r#"image/x" onload="x"#)];
let html = field_html(
&Field::upload("cover", "Cover", HOSTILE),
&Filling::default(),
&Emit::default(),
);
assert!(!html.contains(r#"onload="x"#), "{html}");
}
#[test]
fn the_typed_text_kinds_keep_their_input_type() {
for (kind, expected) in [
(FieldKind::Email, "email"),
(FieldKind::Url, "url"),
(FieldKind::Tel, "tel"),
(FieldKind::Date, "date"),
(FieldKind::DateTime, "datetime-local"),
] {
let html = field_html(&field(kind), &Filling::default(), &Emit::default());
assert!(
html.contains(&format!(r#"type="{expected}""#)),
"{kind:?} emitted {html}"
);
}
}
#[test]
fn a_temporal_field_is_a_native_control_and_not_a_hinted_text_box() {
for kind in [FieldKind::Date, FieldKind::DateTime] {
let html = field_html(&field(kind), &Filling::default(), &Emit::default());
assert!(!html.contains(r#"type="text""#), "{kind:?} emitted {html}");
}
}
#[test]
fn no_prefix_leaves_the_id_as_the_name() {
let html = field_html(
&field(FieldKind::Text),
&Filling::default(),
&Emit::default(),
);
assert!(html.contains(r#"id="title" name="title""#), "{html}");
}
const THEMES: &[makeover_layout::ThemeChoice<'_>] = &[
makeover_layout::ThemeChoice::new(
"goingson",
"GoingsOn",
ThemeVariant::Light,
makeover_layout::Contrast::High,
),
makeover_layout::ThemeChoice::new(
"ayu-light",
"Ayu Light",
ThemeVariant::Light,
makeover_layout::Contrast::Low,
),
makeover_layout::ThemeChoice::new(
"carbonfox",
"Carbonfox",
ThemeVariant::Dark,
makeover_layout::Contrast::High,
),
];
#[test]
fn a_theme_picker_opens_one_optgroup_per_variant() {
let f = Field::theme("theme", "Theme", THEMES);
let html = field_html(&f, &Filling::default(), &Emit::default());
assert_eq!(html.matches("<optgroup").count(), 2, "{html}");
assert_eq!(html.matches("</optgroup>").count(), 2, "{html}");
assert!(
html.contains(r#"<optgroup label="Light" data-variant="light">"#),
"{html}"
);
assert!(
html.contains(r#"<optgroup label="Dark" data-variant="dark">"#),
"{html}"
);
assert!(
html.find("Ayu Light") < html.find("<optgroup label=\"Dark\""),
"{html}"
);
}
#[test]
fn every_theme_carries_its_measured_tier() {
let f = Field::theme("theme", "Theme", THEMES);
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains(r#"data-contrast="high""#), "{html}");
assert!(html.contains(r#"data-contrast="low""#), "{html}");
assert!(html.contains("GoingsOn (AA)"), "{html}");
assert!(html.contains("Ayu Light (low)"), "{html}");
}
#[test]
fn the_follow_row_is_first_and_sits_in_no_group() {
let f =
Field::theme("theme", "Theme", THEMES).following(Choice::new("system", "Follow System"));
let html = field_html(&f, &Filling::default(), &Emit::default());
let follow = html.find("Follow System").expect("the row was offered");
assert!(follow < html.find("<optgroup").expect("groups"), "{html}");
}
#[test]
fn the_stored_theme_is_the_selected_one() {
let f =
Field::theme("theme", "Theme", THEMES).following(Choice::new("system", "Follow System"));
let named = field_html(&f, &Filling::of(Value::Text("carbonfox")), &Emit::default());
assert!(
named.contains(r#"value="carbonfox" data-contrast="high" selected"#),
"{named}"
);
assert!(!named.contains(r#"value="system" selected"#), "{named}");
let following = field_html(&f, &Filling::of(Value::Text("system")), &Emit::default());
assert!(
following.contains(r#"value="system" selected"#),
"{following}"
);
}
#[test]
fn a_theme_that_is_no_longer_installed_keeps_its_value() {
let f = Field::theme("theme", "Theme", THEMES);
let html = field_html(
&f,
&Filling::of(Value::Text("deleted-theme")),
&Emit::default(),
);
assert!(html.contains(r#"data-unmatched="true""#), "{html}");
assert!(
html.contains(r#"<option value="deleted-theme" selected"#),
"{html}"
);
}
#[test]
fn the_follow_row_is_not_a_stray_value() {
let f =
Field::theme("theme", "Theme", THEMES).following(Choice::new("system", "Follow System"));
let html = field_html(&f, &Filling::of(Value::Text("system")), &Emit::default());
assert!(!html.contains("data-unmatched"), "{html}");
}
#[test]
fn a_machine_with_no_themes_still_gets_a_picker() {
let f = Field::theme("theme", "Theme", &[]).following(Choice::new("system", "Follow System"));
let html = field_html(&f, &Filling::default(), &Emit::default());
assert!(html.contains("<select"), "{html}");
assert!(!html.contains("<optgroup"), "{html}");
assert!(html.contains("Follow System"), "{html}");
}