sheetkit_core/
control.rs

1//! Form control support for Excel worksheets.
2//!
3//! Excel uses legacy VML (Vector Markup Language) drawing parts for form
4//! controls such as buttons, check boxes, option buttons, spin buttons,
5//! scroll bars, group boxes, and labels. This module generates the VML
6//! markup needed to add form controls and parses existing VML to read
7//! them back.
8
9use crate::error::{Error, Result};
10use crate::utils::cell_ref::cell_name_to_coordinates;
11
12/// Form control types.
13#[derive(Debug, Clone, PartialEq)]
14pub enum FormControlType {
15    Button,
16    CheckBox,
17    OptionButton,
18    SpinButton,
19    ScrollBar,
20    GroupBox,
21    Label,
22}
23
24impl FormControlType {
25    /// Return the VML ObjectType string for x:ClientData.
26    pub fn object_type(&self) -> &str {
27        match self {
28            FormControlType::Button => "Button",
29            FormControlType::CheckBox => "Checkbox",
30            FormControlType::OptionButton => "Radio",
31            FormControlType::SpinButton => "Spin",
32            FormControlType::ScrollBar => "Scroll",
33            FormControlType::GroupBox => "GBox",
34            FormControlType::Label => "Label",
35        }
36    }
37
38    /// Parse an ObjectType string back into a FormControlType.
39    pub fn from_object_type(s: &str) -> Option<Self> {
40        match s {
41            "Button" => Some(FormControlType::Button),
42            "Checkbox" => Some(FormControlType::CheckBox),
43            "Radio" => Some(FormControlType::OptionButton),
44            "Spin" => Some(FormControlType::SpinButton),
45            "Scroll" => Some(FormControlType::ScrollBar),
46            "GBox" => Some(FormControlType::GroupBox),
47            "Label" => Some(FormControlType::Label),
48            _ => None,
49        }
50    }
51
52    /// Parse a user-facing type string into a FormControlType.
53    pub fn parse(s: &str) -> Result<Self> {
54        match s.to_lowercase().as_str() {
55            "button" => Ok(FormControlType::Button),
56            "checkbox" | "check_box" | "check" => Ok(FormControlType::CheckBox),
57            "optionbutton" | "option_button" | "radio" | "radiobutton" | "radio_button" => {
58                Ok(FormControlType::OptionButton)
59            }
60            "spinbutton" | "spin_button" | "spin" | "spinner" => Ok(FormControlType::SpinButton),
61            "scrollbar" | "scroll_bar" | "scroll" => Ok(FormControlType::ScrollBar),
62            "groupbox" | "group_box" | "group" => Ok(FormControlType::GroupBox),
63            "label" => Ok(FormControlType::Label),
64            _ => Err(Error::InvalidArgument(format!(
65                "unknown form control type: {s}"
66            ))),
67        }
68    }
69}
70
71/// Configuration for adding a form control to a worksheet.
72#[derive(Debug, Clone)]
73pub struct FormControlConfig {
74    /// The type of form control.
75    pub control_type: FormControlType,
76    /// Anchor cell (top-left corner), e.g. "B2".
77    pub cell: String,
78    /// Width in points. Uses a sensible default per control type if None.
79    pub width: Option<f64>,
80    /// Height in points. Uses a sensible default per control type if None.
81    pub height: Option<f64>,
82    /// Display text (Button, CheckBox, OptionButton, GroupBox, Label).
83    pub text: Option<String>,
84    /// VBA macro name (Button only).
85    pub macro_name: Option<String>,
86    /// Linked cell reference for value binding (CheckBox, OptionButton, SpinButton, ScrollBar).
87    pub cell_link: Option<String>,
88    /// Initial checked state (CheckBox, OptionButton).
89    pub checked: Option<bool>,
90    /// Minimum value (SpinButton, ScrollBar).
91    pub min_value: Option<u32>,
92    /// Maximum value (SpinButton, ScrollBar).
93    pub max_value: Option<u32>,
94    /// Step increment (SpinButton, ScrollBar).
95    pub increment: Option<u32>,
96    /// Page increment (ScrollBar only).
97    pub page_increment: Option<u32>,
98    /// Current value (SpinButton, ScrollBar).
99    pub current_value: Option<u32>,
100    /// Enable 3D shading (default true for most controls).
101    pub three_d: Option<bool>,
102}
103
104impl FormControlConfig {
105    /// Create a Button configuration.
106    pub fn button(cell: &str, text: &str) -> Self {
107        Self {
108            control_type: FormControlType::Button,
109            cell: cell.to_string(),
110            width: None,
111            height: None,
112            text: Some(text.to_string()),
113            macro_name: None,
114            cell_link: None,
115            checked: None,
116            min_value: None,
117            max_value: None,
118            increment: None,
119            page_increment: None,
120            current_value: None,
121            three_d: None,
122        }
123    }
124
125    /// Create a CheckBox configuration.
126    pub fn checkbox(cell: &str, text: &str) -> Self {
127        Self {
128            control_type: FormControlType::CheckBox,
129            cell: cell.to_string(),
130            width: None,
131            height: None,
132            text: Some(text.to_string()),
133            macro_name: None,
134            cell_link: None,
135            checked: None,
136            min_value: None,
137            max_value: None,
138            increment: None,
139            page_increment: None,
140            current_value: None,
141            three_d: None,
142        }
143    }
144
145    /// Create a SpinButton configuration.
146    pub fn spin_button(cell: &str, min: u32, max: u32) -> Self {
147        Self {
148            control_type: FormControlType::SpinButton,
149            cell: cell.to_string(),
150            width: None,
151            height: None,
152            text: None,
153            macro_name: None,
154            cell_link: None,
155            checked: None,
156            min_value: Some(min),
157            max_value: Some(max),
158            increment: Some(1),
159            page_increment: None,
160            current_value: Some(min),
161            three_d: None,
162        }
163    }
164
165    /// Create a ScrollBar configuration.
166    pub fn scroll_bar(cell: &str, min: u32, max: u32) -> Self {
167        Self {
168            control_type: FormControlType::ScrollBar,
169            cell: cell.to_string(),
170            width: None,
171            height: None,
172            text: None,
173            macro_name: None,
174            cell_link: None,
175            checked: None,
176            min_value: Some(min),
177            max_value: Some(max),
178            increment: Some(1),
179            page_increment: Some(10),
180            current_value: Some(min),
181            three_d: None,
182        }
183    }
184
185    /// Validate the configuration for correctness.
186    pub fn validate(&self) -> Result<()> {
187        cell_name_to_coordinates(&self.cell)?;
188
189        if let Some(ref cl) = self.cell_link {
190            cell_name_to_coordinates(cl)?;
191        }
192
193        if let (Some(min), Some(max)) = (self.min_value, self.max_value) {
194            if min > max {
195                return Err(Error::InvalidArgument(format!(
196                    "min_value ({min}) must not exceed max_value ({max})"
197                )));
198            }
199        }
200
201        if let Some(inc) = self.increment {
202            if inc == 0 {
203                return Err(Error::InvalidArgument(
204                    "increment must be greater than 0".to_string(),
205                ));
206            }
207        }
208
209        if let Some(page_inc) = self.page_increment {
210            if page_inc == 0 {
211                return Err(Error::InvalidArgument(
212                    "page_increment must be greater than 0".to_string(),
213                ));
214            }
215        }
216
217        Ok(())
218    }
219}
220
221/// Information about an existing form control, returned when querying.
222#[derive(Debug, Clone, PartialEq)]
223pub struct FormControlInfo {
224    pub control_type: FormControlType,
225    pub cell: String,
226    pub text: Option<String>,
227    pub macro_name: Option<String>,
228    pub cell_link: Option<String>,
229    pub checked: Option<bool>,
230    pub current_value: Option<u32>,
231    pub min_value: Option<u32>,
232    pub max_value: Option<u32>,
233    pub increment: Option<u32>,
234    pub page_increment: Option<u32>,
235}
236
237/// Default dimensions (width, height) in points for each control type.
238fn default_dimensions(ct: &FormControlType) -> (f64, f64) {
239    match ct {
240        FormControlType::Button => (72.0, 24.0),
241        FormControlType::CheckBox => (72.0, 18.0),
242        FormControlType::OptionButton => (72.0, 18.0),
243        FormControlType::SpinButton => (15.75, 30.0),
244        FormControlType::ScrollBar => (15.75, 60.0),
245        FormControlType::GroupBox => (120.0, 72.0),
246        FormControlType::Label => (72.0, 18.0),
247    }
248}
249
250/// VML shapetype id for form controls (differs from comments which use t202).
251const FORM_CONTROL_SHAPETYPE_ID: &str = "_x0000_t201";
252
253/// Build the VML anchor string from a cell reference and optional dimensions.
254///
255/// The anchor format is: col1, col1Off, row1, row1Off, col2, col2Off, row2, row2Off.
256/// Offsets are in units of 1/1024 column width or 1/256 row height.
257fn build_control_anchor(cell: &str, width_pt: f64, height_pt: f64) -> Result<String> {
258    let (col, row) = cell_name_to_coordinates(cell)?;
259    let col0 = col - 1;
260    let row0 = row - 1;
261
262    // Approximate column and row span from point dimensions.
263    // Standard column width is ~64 pixels (~48pt), standard row height ~15pt.
264    let col_span = ((width_pt / 48.0).ceil() as u32).max(1);
265    let row_span = ((height_pt / 15.0).ceil() as u32).max(1);
266
267    let col2 = col0 + col_span;
268    let row2 = row0 + row_span;
269
270    Ok(format!("{col0}, 15, {row0}, 10, {col2}, 63, {row2}, 24"))
271}
272
273/// Build a complete VML drawing document containing form control shapes.
274///
275/// `controls` is a list of FormControlConfig entries that have been validated.
276/// `start_shape_id` is the starting shape ID (usually 1025).
277/// Returns the VML XML string.
278pub fn build_form_control_vml(controls: &[FormControlConfig], start_shape_id: usize) -> String {
279    use std::fmt::Write;
280
281    let mut shapes = String::new();
282    for (i, config) in controls.iter().enumerate() {
283        let shape_id = start_shape_id + i;
284        let (default_w, default_h) = default_dimensions(&config.control_type);
285        let width = config.width.unwrap_or(default_w);
286        let height = config.height.unwrap_or(default_h);
287
288        // Skip controls whose cell reference cannot be resolved (e.g. data
289        // from a corrupted file). Validated controls added via
290        // add_form_control will never hit this branch.
291        let anchor = match build_control_anchor(&config.cell, width, height) {
292            Ok(a) => a,
293            Err(_) => {
294                #[cfg(debug_assertions)]
295                eprintln!(
296                    "warning: skipping form control with invalid cell ref '{}'",
297                    config.cell
298                );
299                continue;
300            }
301        };
302
303        write_form_control_shape(&mut shapes, shape_id, i + 1, &anchor, config);
304    }
305
306    let mut doc = String::with_capacity(1024 + shapes.len());
307    doc.push_str("<xml xmlns:v=\"urn:schemas-microsoft-com:vml\"");
308    doc.push_str(" xmlns:o=\"urn:schemas-microsoft-com:office:office\"");
309    doc.push_str(" xmlns:x=\"urn:schemas-microsoft-com:office:excel\">\n");
310    doc.push_str(" <o:shapelayout v:ext=\"edit\">\n");
311    doc.push_str("  <o:idmap v:ext=\"edit\" data=\"1\"/>\n");
312    doc.push_str(" </o:shapelayout>\n");
313
314    // Form control shapetype (t201).
315    let _ = write!(
316        doc,
317        " <v:shapetype id=\"{}\" coordsize=\"21600,21600\" o:spt=\"201\" \
318         path=\"m,l,21600r21600,l21600,xe\">\n\
319         \x20 <v:stroke joinstyle=\"miter\"/>\n\
320         \x20 <v:path gradientshapeok=\"t\" o:connecttype=\"rect\"/>\n\
321         </v:shapetype>\n",
322        FORM_CONTROL_SHAPETYPE_ID,
323    );
324
325    doc.push_str(&shapes);
326    doc.push_str("</xml>\n");
327    doc
328}
329
330/// Write a single VML form control shape element.
331fn write_form_control_shape(
332    out: &mut String,
333    shape_id: usize,
334    z_index: usize,
335    anchor: &str,
336    config: &FormControlConfig,
337) {
338    use std::fmt::Write;
339
340    let _ = write!(out, " <v:shape id=\"_x0000_s{shape_id}\"");
341    let _ = write!(out, " type=\"#{FORM_CONTROL_SHAPETYPE_ID}\"");
342    let _ = write!(
343        out,
344        " style=\"position:absolute;z-index:{z_index};visibility:visible\""
345    );
346
347    match config.control_type {
348        FormControlType::Button => {
349            out.push_str(" fillcolor=\"buttonFace\" o:insetmode=\"auto\">\n");
350            out.push_str("  <v:fill color2=\"buttonFace\" o:detectmouseclick=\"t\"/>\n");
351            out.push_str("  <o:lock v:ext=\"edit\" rotation=\"t\"/>\n");
352            if let Some(ref text) = config.text {
353                let _ = write!(
354                    out,
355                    "  <v:textbox>\n\
356                     \x20  <div style=\"text-align:center\">\
357                     <font face=\"Calibri\" size=\"220\" color=\"#000000\">{text}</font>\
358                     </div>\n\
359                     \x20 </v:textbox>\n"
360                );
361            }
362        }
363        FormControlType::CheckBox | FormControlType::OptionButton => {
364            out.push_str(" fillcolor=\"window\" o:insetmode=\"auto\">\n");
365            out.push_str("  <v:fill color2=\"window\"/>\n");
366            if let Some(ref text) = config.text {
367                let _ = write!(
368                    out,
369                    "  <v:textbox>\n\
370                     \x20  <div>{text}</div>\n\
371                     \x20 </v:textbox>\n"
372                );
373            }
374        }
375        FormControlType::SpinButton | FormControlType::ScrollBar => {
376            out.push_str(" fillcolor=\"buttonFace\" o:insetmode=\"auto\">\n");
377            out.push_str("  <v:fill color2=\"buttonFace\"/>\n");
378        }
379        FormControlType::GroupBox => {
380            out.push_str(" filled=\"f\" stroked=\"f\" o:insetmode=\"auto\">\n");
381            if let Some(ref text) = config.text {
382                let _ = write!(
383                    out,
384                    "  <v:textbox>\n\
385                     \x20  <div>{text}</div>\n\
386                     \x20 </v:textbox>\n"
387                );
388            }
389        }
390        FormControlType::Label => {
391            out.push_str(" filled=\"f\" stroked=\"f\" o:insetmode=\"auto\">\n");
392            if let Some(ref text) = config.text {
393                let _ = write!(
394                    out,
395                    "  <v:textbox>\n\
396                     \x20  <div>{text}</div>\n\
397                     \x20 </v:textbox>\n"
398                );
399            }
400        }
401    }
402
403    // x:ClientData element with control-specific properties.
404    let object_type = config.control_type.object_type();
405    let _ = writeln!(out, "  <x:ClientData ObjectType=\"{object_type}\">");
406    let _ = writeln!(out, "   <x:Anchor>{anchor}</x:Anchor>");
407    out.push_str("   <x:PrintObject>False</x:PrintObject>\n");
408    out.push_str("   <x:AutoFill>False</x:AutoFill>\n");
409
410    if let Some(ref macro_name) = config.macro_name {
411        let _ = writeln!(out, "   <x:FmlaMacro>{macro_name}</x:FmlaMacro>");
412    }
413
414    if let Some(ref cell_link) = config.cell_link {
415        let _ = writeln!(out, "   <x:FmlaLink>{cell_link}</x:FmlaLink>");
416    }
417
418    if let Some(checked) = config.checked {
419        let val = if checked { 1 } else { 0 };
420        let _ = writeln!(out, "   <x:Checked>{val}</x:Checked>");
421    }
422
423    if let Some(val) = config.current_value {
424        let _ = writeln!(out, "   <x:Val>{val}</x:Val>");
425    }
426
427    if let Some(min) = config.min_value {
428        let _ = writeln!(out, "   <x:Min>{min}</x:Min>");
429    }
430
431    if let Some(max) = config.max_value {
432        let _ = writeln!(out, "   <x:Max>{max}</x:Max>");
433    }
434
435    if let Some(inc) = config.increment {
436        let _ = writeln!(out, "   <x:Inc>{inc}</x:Inc>");
437    }
438
439    if let Some(page_inc) = config.page_increment {
440        let _ = writeln!(out, "   <x:Page>{page_inc}</x:Page>");
441    }
442
443    // 3D shading: default is true for most controls. Write NoThreeD only when explicitly false.
444    let three_d = config.three_d.unwrap_or(true);
445    if !three_d {
446        out.push_str("   <x:NoThreeD/>\n");
447    }
448
449    out.push_str("  </x:ClientData>\n");
450    out.push_str(" </v:shape>\n");
451}
452
453/// Parse form controls from a VML drawing XML string.
454///
455/// Scans for `<x:ClientData ObjectType="...">` elements and extracts
456/// control properties. Returns a list of FormControlInfo.
457pub fn parse_form_controls(vml_xml: &str) -> Vec<FormControlInfo> {
458    let mut controls = Vec::new();
459
460    let mut search_from = 0;
461    while let Some(shape_start) = vml_xml[search_from..].find("<v:shape ") {
462        let abs_start = search_from + shape_start;
463        let shape_end = match vml_xml[abs_start..].find("</v:shape>") {
464            Some(pos) => abs_start + pos + "</v:shape>".len(),
465            None => break,
466        };
467        let shape_xml = &vml_xml[abs_start..shape_end];
468
469        // Skip non-form-control shapes (e.g. comment shapes use ObjectType="Note").
470        if let Some(info) = parse_single_control(shape_xml) {
471            controls.push(info);
472        }
473        search_from = shape_end;
474    }
475
476    controls
477}
478
479/// Parse a single v:shape element into a FormControlInfo, if it is a form control.
480fn parse_single_control(shape_xml: &str) -> Option<FormControlInfo> {
481    // Find the ClientData element.
482    let cd_start = shape_xml.find("<x:ClientData ")?;
483    let cd_end = shape_xml
484        .find("</x:ClientData>")
485        .map(|p| p + "</x:ClientData>".len())?;
486    let cd_xml = &shape_xml[cd_start..cd_end];
487
488    // Extract ObjectType attribute.
489    let obj_type = extract_attr(cd_xml, "ObjectType")?;
490    let control_type = FormControlType::from_object_type(&obj_type)?;
491
492    // Skip Note types (comments).
493    if obj_type == "Note" {
494        return None;
495    }
496
497    let cell = extract_anchor_cell(cd_xml).unwrap_or_default();
498    let text = extract_textbox_text(shape_xml);
499    let macro_name = extract_element(cd_xml, "x:FmlaMacro");
500    let cell_link = extract_element(cd_xml, "x:FmlaLink");
501    let checked = extract_element(cd_xml, "x:Checked").and_then(|v| match v.as_str() {
502        "1" => Some(true),
503        "0" => Some(false),
504        _ => None,
505    });
506    let current_value = extract_element(cd_xml, "x:Val").and_then(|v| v.parse().ok());
507    let min_value = extract_element(cd_xml, "x:Min").and_then(|v| v.parse().ok());
508    let max_value = extract_element(cd_xml, "x:Max").and_then(|v| v.parse().ok());
509    let increment = extract_element(cd_xml, "x:Inc").and_then(|v| v.parse().ok());
510    let page_increment = extract_element(cd_xml, "x:Page").and_then(|v| v.parse().ok());
511
512    Some(FormControlInfo {
513        control_type,
514        cell,
515        text,
516        macro_name,
517        cell_link,
518        checked,
519        current_value,
520        min_value,
521        max_value,
522        increment,
523        page_increment,
524    })
525}
526
527/// Extract an XML attribute value from a tag.
528fn extract_attr(xml: &str, attr: &str) -> Option<String> {
529    let pattern = format!("{attr}=\"");
530    let start = xml.find(&pattern)?;
531    let val_start = start + pattern.len();
532    let end = xml[val_start..].find('"')?;
533    Some(xml[val_start..val_start + end].to_string())
534}
535
536/// Extract text content of an XML element like `<tag>content</tag>`.
537fn extract_element(xml: &str, tag: &str) -> Option<String> {
538    let open = format!("<{tag}>");
539    let close = format!("</{tag}>");
540    let start = xml.find(&open)?;
541    let content_start = start + open.len();
542    let end = xml[content_start..].find(&close)?;
543    let text = xml[content_start..content_start + end].trim().to_string();
544    if text.is_empty() {
545        None
546    } else {
547        Some(text)
548    }
549}
550
551/// Extract textbox text from a v:shape element.
552fn extract_textbox_text(shape_xml: &str) -> Option<String> {
553    let tb_start = shape_xml.find("<v:textbox>")?;
554    let tb_end = shape_xml.find("</v:textbox>")?;
555    let tb_content = &shape_xml[tb_start + "<v:textbox>".len()..tb_end];
556
557    // Text is inside a <div> or <font> element; extract plain text.
558    let mut text = String::new();
559    let mut in_tag = false;
560    for ch in tb_content.chars() {
561        match ch {
562            '<' => in_tag = true,
563            '>' => in_tag = false,
564            _ if !in_tag => text.push(ch),
565            _ => {}
566        }
567    }
568    let trimmed = text.trim().to_string();
569    if trimmed.is_empty() {
570        None
571    } else {
572        Some(trimmed)
573    }
574}
575
576/// Extract the anchor cell reference from x:Anchor element.
577///
578/// The anchor format is "col1, col1Off, row1, row1Off, col2, col2Off, row2, row2Off".
579/// We derive the cell from col1 (0-based) and row1 (0-based).
580fn extract_anchor_cell(cd_xml: &str) -> Option<String> {
581    let anchor_text = extract_element(cd_xml, "x:Anchor")?;
582    let parts: Vec<&str> = anchor_text.split(',').map(|s| s.trim()).collect();
583    if parts.len() < 4 {
584        return None;
585    }
586    let col0: u32 = parts[0].parse().ok()?;
587    let row0: u32 = parts[2].parse().ok()?;
588    crate::utils::cell_ref::coordinates_to_cell_name(col0 + 1, row0 + 1).ok()
589}
590
591/// Merge new form control VML into existing VML bytes (for sheets that
592/// already have VML content from comments or other controls).
593///
594/// This appends new shape elements before the closing `</xml>` tag and
595/// updates the shapetype if needed.
596pub fn merge_vml_controls(
597    existing_vml: &[u8],
598    controls: &[FormControlConfig],
599    start_shape_id: usize,
600) -> Vec<u8> {
601    let existing_str = String::from_utf8_lossy(existing_vml);
602
603    // Generate shape elements for the new controls.
604    let mut shapes = String::new();
605    for (i, config) in controls.iter().enumerate() {
606        let shape_id = start_shape_id + i;
607        let (default_w, default_h) = default_dimensions(&config.control_type);
608        let width = config.width.unwrap_or(default_w);
609        let height = config.height.unwrap_or(default_h);
610
611        if let Ok(anchor) = build_control_anchor(&config.cell, width, height) {
612            write_form_control_shape(&mut shapes, shape_id, shape_id, &anchor, config);
613        }
614    }
615
616    // Check if the form control shapetype already exists.
617    let shapetype_exists = existing_str.contains(FORM_CONTROL_SHAPETYPE_ID);
618
619    let shapetype_xml = if !shapetype_exists {
620        format!(
621            " <v:shapetype id=\"{FORM_CONTROL_SHAPETYPE_ID}\" coordsize=\"21600,21600\" \
622             o:spt=\"201\" path=\"m,l,21600r21600,l21600,xe\">\n\
623             \x20 <v:stroke joinstyle=\"miter\"/>\n\
624             \x20 <v:path gradientshapeok=\"t\" o:connecttype=\"rect\"/>\n\
625             </v:shapetype>\n"
626        )
627    } else {
628        String::new()
629    };
630
631    // Insert before closing </xml>.
632    if let Some(close_pos) = existing_str.rfind("</xml>") {
633        let mut result =
634            String::with_capacity(existing_str.len() + shapetype_xml.len() + shapes.len());
635        result.push_str(&existing_str[..close_pos]);
636        result.push_str(&shapetype_xml);
637        result.push_str(&shapes);
638        result.push_str("</xml>\n");
639        result.into_bytes()
640    } else {
641        // Malformed VML; return new VML document.
642        build_form_control_vml(controls, start_shape_id).into_bytes()
643    }
644}
645
646impl FormControlInfo {
647    /// Convert a parsed `FormControlInfo` back to a `FormControlConfig`.
648    ///
649    /// This is used during VML hydration to reconstruct the config list from
650    /// existing VML data so that subsequent add/delete operations work correctly.
651    pub fn to_config(&self) -> FormControlConfig {
652        FormControlConfig {
653            control_type: self.control_type.clone(),
654            cell: self.cell.clone(),
655            width: None,
656            height: None,
657            text: self.text.clone(),
658            macro_name: self.macro_name.clone(),
659            cell_link: self.cell_link.clone(),
660            checked: self.checked,
661            min_value: self.min_value,
662            max_value: self.max_value,
663            increment: self.increment,
664            page_increment: self.page_increment,
665            current_value: self.current_value,
666            three_d: None,
667        }
668    }
669}
670
671/// Count existing VML shapes in VML bytes to determine the next shape ID.
672pub fn count_vml_shapes(vml_bytes: &[u8]) -> usize {
673    let vml_str = String::from_utf8_lossy(vml_bytes);
674    vml_str.matches("<v:shape ").count()
675}
676
677/// Strip form control shapes from VML bytes, keeping only comment (Note) shapes.
678///
679/// Returns `None` if no comment shapes remain after stripping, or `Some(bytes)`
680/// with the cleaned VML that contains only comment shapes.
681pub fn strip_form_control_shapes_from_vml(vml_bytes: &[u8]) -> Option<Vec<u8>> {
682    let vml_str = String::from_utf8_lossy(vml_bytes);
683
684    // Collect ranges of form control shapes to remove.
685    let mut keep_shapes = Vec::new();
686    let mut has_comment_shapes = false;
687    let mut search_from = 0;
688
689    while let Some(shape_start) = vml_str[search_from..].find("<v:shape ") {
690        let abs_start = search_from + shape_start;
691        let shape_end = match vml_str[abs_start..].find("</v:shape>") {
692            Some(pos) => abs_start + pos + "</v:shape>".len(),
693            None => break,
694        };
695        let shape_xml = &vml_str[abs_start..shape_end];
696
697        // Check if this is a comment shape (ObjectType="Note").
698        if shape_xml.contains("ObjectType=\"Note\"") {
699            keep_shapes.push((abs_start, shape_end));
700            has_comment_shapes = true;
701        }
702        // Form control shapes are silently dropped.
703        search_from = shape_end;
704    }
705
706    if !has_comment_shapes {
707        return None;
708    }
709
710    // Rebuild VML with only the comment shapes preserved.
711    // Find the header portion (everything before the first <v:shape).
712    let first_shape_pos = vml_str.find("<v:shape ").unwrap_or(vml_str.len());
713    let header = &vml_str[..first_shape_pos];
714
715    // Also strip the form control shapetype (_x0000_t201) if present, keeping
716    // only the comment shapetype (_x0000_t202).
717    let header = remove_shapetype_block(header, FORM_CONTROL_SHAPETYPE_ID);
718
719    let mut result = String::with_capacity(vml_str.len());
720    result.push_str(&header);
721
722    for (start, end) in &keep_shapes {
723        result.push_str(&vml_str[*start..*end]);
724        result.push('\n');
725    }
726
727    result.push_str("</xml>\n");
728    Some(result.into_bytes())
729}
730
731/// Remove a <v:shapetype id="..."> ... </v:shapetype> block from the header.
732fn remove_shapetype_block(header: &str, shapetype_id: &str) -> String {
733    if let Some(st_start) = header.find(&format!("<v:shapetype id=\"{shapetype_id}\"")) {
734        let st_end_tag = "</v:shapetype>";
735        if let Some(rel_end) = header[st_start..].find(st_end_tag) {
736            let st_end = st_start + rel_end + st_end_tag.len();
737            // Also consume a trailing newline if present.
738            let st_end = if header.as_bytes().get(st_end) == Some(&b'\n') {
739                st_end + 1
740            } else {
741                st_end
742            };
743            let mut cleaned = String::with_capacity(header.len());
744            cleaned.push_str(&header[..st_start]);
745            cleaned.push_str(&header[st_end..]);
746            return cleaned;
747        }
748    }
749    header.to_string()
750}
751
752#[cfg(test)]
753#[allow(clippy::cloned_ref_to_slice_refs)]
754mod tests {
755    use super::*;
756
757    #[test]
758    fn test_form_control_type_parse() {
759        assert_eq!(
760            FormControlType::parse("button").unwrap(),
761            FormControlType::Button
762        );
763        assert_eq!(
764            FormControlType::parse("Button").unwrap(),
765            FormControlType::Button
766        );
767        assert_eq!(
768            FormControlType::parse("checkbox").unwrap(),
769            FormControlType::CheckBox
770        );
771        assert_eq!(
772            FormControlType::parse("check_box").unwrap(),
773            FormControlType::CheckBox
774        );
775        assert_eq!(
776            FormControlType::parse("radio").unwrap(),
777            FormControlType::OptionButton
778        );
779        assert_eq!(
780            FormControlType::parse("optionButton").unwrap(),
781            FormControlType::OptionButton
782        );
783        assert_eq!(
784            FormControlType::parse("spin").unwrap(),
785            FormControlType::SpinButton
786        );
787        assert_eq!(
788            FormControlType::parse("spinner").unwrap(),
789            FormControlType::SpinButton
790        );
791        assert_eq!(
792            FormControlType::parse("scroll").unwrap(),
793            FormControlType::ScrollBar
794        );
795        assert_eq!(
796            FormControlType::parse("scrollbar").unwrap(),
797            FormControlType::ScrollBar
798        );
799        assert_eq!(
800            FormControlType::parse("group").unwrap(),
801            FormControlType::GroupBox
802        );
803        assert_eq!(
804            FormControlType::parse("groupbox").unwrap(),
805            FormControlType::GroupBox
806        );
807        assert_eq!(
808            FormControlType::parse("label").unwrap(),
809            FormControlType::Label
810        );
811        assert!(FormControlType::parse("unknown").is_err());
812    }
813
814    #[test]
815    fn test_form_control_type_object_type() {
816        assert_eq!(FormControlType::Button.object_type(), "Button");
817        assert_eq!(FormControlType::CheckBox.object_type(), "Checkbox");
818        assert_eq!(FormControlType::OptionButton.object_type(), "Radio");
819        assert_eq!(FormControlType::SpinButton.object_type(), "Spin");
820        assert_eq!(FormControlType::ScrollBar.object_type(), "Scroll");
821        assert_eq!(FormControlType::GroupBox.object_type(), "GBox");
822        assert_eq!(FormControlType::Label.object_type(), "Label");
823    }
824
825    #[test]
826    fn test_form_control_type_roundtrip() {
827        let types = vec![
828            FormControlType::Button,
829            FormControlType::CheckBox,
830            FormControlType::OptionButton,
831            FormControlType::SpinButton,
832            FormControlType::ScrollBar,
833            FormControlType::GroupBox,
834            FormControlType::Label,
835        ];
836        for ct in types {
837            let obj_type = ct.object_type();
838            let parsed = FormControlType::from_object_type(obj_type).unwrap();
839            assert_eq!(parsed, ct);
840        }
841    }
842
843    #[test]
844    fn test_validate_config_valid() {
845        let config = FormControlConfig::button("A1", "Click Me");
846        assert!(config.validate().is_ok());
847    }
848
849    #[test]
850    fn test_validate_config_invalid_cell() {
851        let mut config = FormControlConfig::button("INVALID", "Click Me");
852        config.cell = "ZZZZZ".to_string();
853        assert!(config.validate().is_err());
854    }
855
856    #[test]
857    fn test_validate_config_min_exceeds_max() {
858        let mut config = FormControlConfig::spin_button("A1", 0, 100);
859        config.min_value = Some(200);
860        config.max_value = Some(100);
861        assert!(config.validate().is_err());
862    }
863
864    #[test]
865    fn test_validate_config_zero_increment() {
866        let mut config = FormControlConfig::spin_button("A1", 0, 100);
867        config.increment = Some(0);
868        assert!(config.validate().is_err());
869    }
870
871    #[test]
872    fn test_validate_config_zero_page_increment() {
873        let mut config = FormControlConfig::scroll_bar("A1", 0, 100);
874        config.page_increment = Some(0);
875        assert!(config.validate().is_err());
876    }
877
878    #[test]
879    fn test_validate_config_invalid_cell_link() {
880        let mut config = FormControlConfig::checkbox("A1", "Check");
881        config.cell_link = Some("NOT_A_CELL".to_string());
882        assert!(config.validate().is_err());
883    }
884
885    #[test]
886    fn test_build_button_vml() {
887        let config = FormControlConfig::button("B2", "Click Me");
888        let vml = build_form_control_vml(&[config], 1025);
889
890        assert!(vml.contains("xmlns:v=\"urn:schemas-microsoft-com:vml\""));
891        assert!(vml.contains("xmlns:o=\"urn:schemas-microsoft-com:office:office\""));
892        assert!(vml.contains("xmlns:x=\"urn:schemas-microsoft-com:office:excel\""));
893        assert!(vml.contains("ObjectType=\"Button\""));
894        assert!(vml.contains("Click Me"));
895        assert!(vml.contains("_x0000_s1025"));
896        assert!(vml.contains("_x0000_t201"));
897        assert!(vml.contains("fillcolor=\"buttonFace\""));
898    }
899
900    #[test]
901    fn test_build_checkbox_vml() {
902        let mut config = FormControlConfig::checkbox("A1", "Enable Feature");
903        config.cell_link = Some("$C$1".to_string());
904        config.checked = Some(true);
905
906        let vml = build_form_control_vml(&[config], 1025);
907        assert!(vml.contains("ObjectType=\"Checkbox\""));
908        assert!(vml.contains("Enable Feature"));
909        assert!(vml.contains("<x:FmlaLink>$C$1</x:FmlaLink>"));
910        assert!(vml.contains("<x:Checked>1</x:Checked>"));
911    }
912
913    #[test]
914    fn test_build_option_button_vml() {
915        let config = FormControlConfig {
916            control_type: FormControlType::OptionButton,
917            cell: "A3".to_string(),
918            width: None,
919            height: None,
920            text: Some("Option A".to_string()),
921            macro_name: None,
922            cell_link: Some("$D$1".to_string()),
923            checked: Some(false),
924            min_value: None,
925            max_value: None,
926            increment: None,
927            page_increment: None,
928            current_value: None,
929            three_d: None,
930        };
931
932        let vml = build_form_control_vml(&[config], 1025);
933        assert!(vml.contains("ObjectType=\"Radio\""));
934        assert!(vml.contains("Option A"));
935        assert!(vml.contains("<x:FmlaLink>$D$1</x:FmlaLink>"));
936        assert!(vml.contains("<x:Checked>0</x:Checked>"));
937    }
938
939    #[test]
940    fn test_build_spin_button_vml() {
941        let config = FormControlConfig::spin_button("E1", 0, 100);
942        let vml = build_form_control_vml(&[config], 1025);
943
944        assert!(vml.contains("ObjectType=\"Spin\""));
945        assert!(vml.contains("<x:Min>0</x:Min>"));
946        assert!(vml.contains("<x:Max>100</x:Max>"));
947        assert!(vml.contains("<x:Inc>1</x:Inc>"));
948        assert!(vml.contains("<x:Val>0</x:Val>"));
949    }
950
951    #[test]
952    fn test_build_scroll_bar_vml() {
953        let config = FormControlConfig::scroll_bar("F1", 10, 200);
954        let vml = build_form_control_vml(&[config], 1025);
955
956        assert!(vml.contains("ObjectType=\"Scroll\""));
957        assert!(vml.contains("<x:Min>10</x:Min>"));
958        assert!(vml.contains("<x:Max>200</x:Max>"));
959        assert!(vml.contains("<x:Inc>1</x:Inc>"));
960        assert!(vml.contains("<x:Page>10</x:Page>"));
961    }
962
963    #[test]
964    fn test_build_group_box_vml() {
965        let config = FormControlConfig {
966            control_type: FormControlType::GroupBox,
967            cell: "A1".to_string(),
968            width: None,
969            height: None,
970            text: Some("Options".to_string()),
971            macro_name: None,
972            cell_link: None,
973            checked: None,
974            min_value: None,
975            max_value: None,
976            increment: None,
977            page_increment: None,
978            current_value: None,
979            three_d: None,
980        };
981
982        let vml = build_form_control_vml(&[config], 1025);
983        assert!(vml.contains("ObjectType=\"GBox\""));
984        assert!(vml.contains("Options"));
985        assert!(vml.contains("filled=\"f\""));
986    }
987
988    #[test]
989    fn test_build_label_vml() {
990        let config = FormControlConfig {
991            control_type: FormControlType::Label,
992            cell: "A1".to_string(),
993            width: None,
994            height: None,
995            text: Some("Status:".to_string()),
996            macro_name: None,
997            cell_link: None,
998            checked: None,
999            min_value: None,
1000            max_value: None,
1001            increment: None,
1002            page_increment: None,
1003            current_value: None,
1004            three_d: None,
1005        };
1006
1007        let vml = build_form_control_vml(&[config], 1025);
1008        assert!(vml.contains("ObjectType=\"Label\""));
1009        assert!(vml.contains("Status:"));
1010    }
1011
1012    #[test]
1013    fn test_build_button_with_macro() {
1014        let mut config = FormControlConfig::button("A1", "Run Macro");
1015        config.macro_name = Some("Sheet1.MyMacro".to_string());
1016
1017        let vml = build_form_control_vml(&[config], 1025);
1018        assert!(vml.contains("<x:FmlaMacro>Sheet1.MyMacro</x:FmlaMacro>"));
1019    }
1020
1021    #[test]
1022    fn test_build_control_no_three_d() {
1023        let mut config = FormControlConfig::checkbox("A1", "Flat");
1024        config.three_d = Some(false);
1025
1026        let vml = build_form_control_vml(&[config], 1025);
1027        assert!(vml.contains("<x:NoThreeD/>"));
1028    }
1029
1030    #[test]
1031    fn test_build_multiple_controls() {
1032        let controls = vec![
1033            FormControlConfig::button("A1", "Button 1"),
1034            FormControlConfig::checkbox("A3", "Check 1"),
1035            FormControlConfig::spin_button("C1", 0, 50),
1036        ];
1037
1038        let vml = build_form_control_vml(&controls, 1025);
1039        assert!(vml.contains("_x0000_s1025"));
1040        assert!(vml.contains("_x0000_s1026"));
1041        assert!(vml.contains("_x0000_s1027"));
1042        assert!(vml.contains("ObjectType=\"Button\""));
1043        assert!(vml.contains("ObjectType=\"Checkbox\""));
1044        assert!(vml.contains("ObjectType=\"Spin\""));
1045    }
1046
1047    #[test]
1048    fn test_parse_form_controls_button() {
1049        let config = FormControlConfig::button("B2", "Click Me");
1050        let vml = build_form_control_vml(&[config], 1025);
1051
1052        let controls = parse_form_controls(&vml);
1053        assert_eq!(controls.len(), 1);
1054        assert_eq!(controls[0].control_type, FormControlType::Button);
1055        assert_eq!(controls[0].text.as_deref(), Some("Click Me"));
1056    }
1057
1058    #[test]
1059    fn test_parse_form_controls_checkbox_with_link() {
1060        let mut config = FormControlConfig::checkbox("A1", "Toggle");
1061        config.cell_link = Some("$D$1".to_string());
1062        config.checked = Some(true);
1063        let vml = build_form_control_vml(&[config], 1025);
1064
1065        let controls = parse_form_controls(&vml);
1066        assert_eq!(controls.len(), 1);
1067        assert_eq!(controls[0].control_type, FormControlType::CheckBox);
1068        assert_eq!(controls[0].text.as_deref(), Some("Toggle"));
1069        assert_eq!(controls[0].cell_link.as_deref(), Some("$D$1"));
1070        assert_eq!(controls[0].checked, Some(true));
1071    }
1072
1073    #[test]
1074    fn test_parse_form_controls_spin_button() {
1075        let config = FormControlConfig::spin_button("C1", 5, 50);
1076        let vml = build_form_control_vml(&[config], 1025);
1077
1078        let controls = parse_form_controls(&vml);
1079        assert_eq!(controls.len(), 1);
1080        assert_eq!(controls[0].control_type, FormControlType::SpinButton);
1081        assert_eq!(controls[0].min_value, Some(5));
1082        assert_eq!(controls[0].max_value, Some(50));
1083        assert_eq!(controls[0].increment, Some(1));
1084        assert_eq!(controls[0].current_value, Some(5));
1085    }
1086
1087    #[test]
1088    fn test_parse_form_controls_scroll_bar() {
1089        let config = FormControlConfig::scroll_bar("E1", 0, 100);
1090        let vml = build_form_control_vml(&[config], 1025);
1091
1092        let controls = parse_form_controls(&vml);
1093        assert_eq!(controls.len(), 1);
1094        assert_eq!(controls[0].control_type, FormControlType::ScrollBar);
1095        assert_eq!(controls[0].page_increment, Some(10));
1096    }
1097
1098    #[test]
1099    fn test_parse_multiple_controls() {
1100        let controls = vec![
1101            FormControlConfig::button("A1", "Btn"),
1102            FormControlConfig::checkbox("A3", "Chk"),
1103            FormControlConfig::spin_button("C1", 0, 10),
1104        ];
1105        let vml = build_form_control_vml(&controls, 1025);
1106
1107        let parsed = parse_form_controls(&vml);
1108        assert_eq!(parsed.len(), 3);
1109        assert_eq!(parsed[0].control_type, FormControlType::Button);
1110        assert_eq!(parsed[1].control_type, FormControlType::CheckBox);
1111        assert_eq!(parsed[2].control_type, FormControlType::SpinButton);
1112    }
1113
1114    #[test]
1115    fn test_parse_ignores_comment_shapes() {
1116        // Build a VML that has both a comment (Note) and a form control.
1117        let comment_vml = crate::vml::build_vml_drawing(&["A1"]);
1118        let controls = parse_form_controls(&comment_vml);
1119        assert!(controls.is_empty(), "comment shapes should be ignored");
1120    }
1121
1122    #[test]
1123    fn test_count_vml_shapes() {
1124        let vml = build_form_control_vml(
1125            &[
1126                FormControlConfig::button("A1", "B1"),
1127                FormControlConfig::checkbox("A3", "C1"),
1128            ],
1129            1025,
1130        );
1131        assert_eq!(count_vml_shapes(vml.as_bytes()), 2);
1132    }
1133
1134    #[test]
1135    fn test_merge_vml_controls() {
1136        let existing = build_form_control_vml(&[FormControlConfig::button("A1", "First")], 1025);
1137        let new_controls = vec![FormControlConfig::checkbox("A3", "Second")];
1138        let merged = merge_vml_controls(existing.as_bytes(), &new_controls, 1026);
1139        let merged_str = String::from_utf8(merged).unwrap();
1140
1141        assert!(merged_str.contains("_x0000_s1025"));
1142        assert!(merged_str.contains("_x0000_s1026"));
1143        assert!(merged_str.contains("ObjectType=\"Button\""));
1144        assert!(merged_str.contains("ObjectType=\"Checkbox\""));
1145        // Should not duplicate shapetype.
1146        let shapetype_count = merged_str.matches(FORM_CONTROL_SHAPETYPE_ID).count();
1147        // One in the shapetype definition and references in each shape = 1 (def) + 2 (refs)
1148        assert!(shapetype_count >= 2);
1149    }
1150
1151    #[test]
1152    fn test_build_control_anchor_basic() {
1153        let anchor = build_control_anchor("A1", 72.0, 24.0).unwrap();
1154        let parts: Vec<&str> = anchor.split(", ").collect();
1155        assert_eq!(parts.len(), 8);
1156        assert_eq!(parts[0], "0"); // col0
1157        assert_eq!(parts[2], "0"); // row0
1158    }
1159
1160    #[test]
1161    fn test_build_control_anchor_offset_cell() {
1162        let anchor = build_control_anchor("C5", 72.0, 30.0).unwrap();
1163        let parts: Vec<&str> = anchor.split(", ").collect();
1164        assert_eq!(parts[0], "2"); // col0 (C = 3, 0-based = 2)
1165        assert_eq!(parts[2], "4"); // row0 (5, 0-based = 4)
1166    }
1167
1168    #[test]
1169    fn test_build_control_anchor_invalid_cell() {
1170        assert!(build_control_anchor("INVALID", 72.0, 24.0).is_err());
1171    }
1172
1173    #[test]
1174    fn test_default_dimensions() {
1175        let (w, h) = default_dimensions(&FormControlType::Button);
1176        assert_eq!(w, 72.0);
1177        assert_eq!(h, 24.0);
1178
1179        let (w, h) = default_dimensions(&FormControlType::ScrollBar);
1180        assert_eq!(w, 15.75);
1181        assert_eq!(h, 60.0);
1182    }
1183
1184    #[test]
1185    fn test_extract_anchor_cell() {
1186        let cd = "<x:ClientData ObjectType=\"Button\"><x:Anchor>1, 15, 0, 10, 3, 63, 2, 24</x:Anchor></x:ClientData>";
1187        let cell = extract_anchor_cell(cd).unwrap();
1188        assert_eq!(cell, "B1");
1189    }
1190
1191    #[test]
1192    fn test_custom_dimensions() {
1193        let mut config = FormControlConfig::button("A1", "Wide");
1194        config.width = Some(200.0);
1195        config.height = Some(50.0);
1196        let vml = build_form_control_vml(&[config], 1025);
1197        assert!(vml.contains("_x0000_s1025"));
1198    }
1199
1200    #[test]
1201    fn test_workbook_add_form_control() {
1202        use crate::workbook::Workbook;
1203
1204        let mut wb = Workbook::new();
1205        let config = FormControlConfig::button("B2", "Click Me");
1206        wb.add_form_control("Sheet1", config).unwrap();
1207
1208        let controls = wb.get_form_controls("Sheet1").unwrap();
1209        assert_eq!(controls.len(), 1);
1210        assert_eq!(controls[0].control_type, FormControlType::Button);
1211        assert_eq!(controls[0].text.as_deref(), Some("Click Me"));
1212    }
1213
1214    #[test]
1215    fn test_workbook_add_multiple_form_controls() {
1216        use crate::workbook::Workbook;
1217
1218        let mut wb = Workbook::new();
1219        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Button 1"))
1220            .unwrap();
1221        wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Check 1"))
1222            .unwrap();
1223        wb.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 100))
1224            .unwrap();
1225
1226        let controls = wb.get_form_controls("Sheet1").unwrap();
1227        assert_eq!(controls.len(), 3);
1228        assert_eq!(controls[0].control_type, FormControlType::Button);
1229        assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1230        assert_eq!(controls[2].control_type, FormControlType::SpinButton);
1231    }
1232
1233    #[test]
1234    fn test_workbook_add_form_control_sheet_not_found() {
1235        use crate::workbook::Workbook;
1236
1237        let mut wb = Workbook::new();
1238        let config = FormControlConfig::button("A1", "Test");
1239        let result = wb.add_form_control("NoSheet", config);
1240        assert!(result.is_err());
1241    }
1242
1243    #[test]
1244    fn test_workbook_add_form_control_invalid_cell() {
1245        use crate::workbook::Workbook;
1246
1247        let mut wb = Workbook::new();
1248        let config = FormControlConfig {
1249            control_type: FormControlType::Button,
1250            cell: "INVALID".to_string(),
1251            width: None,
1252            height: None,
1253            text: Some("Test".to_string()),
1254            macro_name: None,
1255            cell_link: None,
1256            checked: None,
1257            min_value: None,
1258            max_value: None,
1259            increment: None,
1260            page_increment: None,
1261            current_value: None,
1262            three_d: None,
1263        };
1264        let result = wb.add_form_control("Sheet1", config);
1265        assert!(result.is_err());
1266    }
1267
1268    #[test]
1269    fn test_workbook_delete_form_control() {
1270        use crate::workbook::Workbook;
1271
1272        let mut wb = Workbook::new();
1273        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn 1"))
1274            .unwrap();
1275        wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Chk 1"))
1276            .unwrap();
1277
1278        wb.delete_form_control("Sheet1", 0).unwrap();
1279
1280        let controls = wb.get_form_controls("Sheet1").unwrap();
1281        assert_eq!(controls.len(), 1);
1282        assert_eq!(controls[0].control_type, FormControlType::CheckBox);
1283    }
1284
1285    #[test]
1286    fn test_workbook_delete_form_control_out_of_bounds() {
1287        use crate::workbook::Workbook;
1288
1289        let mut wb = Workbook::new();
1290        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn"))
1291            .unwrap();
1292        let result = wb.delete_form_control("Sheet1", 5);
1293        assert!(result.is_err());
1294    }
1295
1296    #[test]
1297    fn test_workbook_form_control_save_roundtrip() {
1298        use crate::workbook::Workbook;
1299        use tempfile::TempDir;
1300
1301        let dir = TempDir::new().unwrap();
1302        let path = dir.path().join("form_controls.xlsx");
1303
1304        let mut wb = Workbook::new();
1305        let mut btn = FormControlConfig::button("B2", "Submit");
1306        btn.macro_name = Some("Sheet1.OnSubmit".to_string());
1307        wb.add_form_control("Sheet1", btn).unwrap();
1308
1309        let mut chk = FormControlConfig::checkbox("B4", "Agree");
1310        chk.cell_link = Some("$D$4".to_string());
1311        chk.checked = Some(true);
1312        wb.add_form_control("Sheet1", chk).unwrap();
1313
1314        wb.add_form_control("Sheet1", FormControlConfig::spin_button("E2", 0, 100))
1315            .unwrap();
1316
1317        wb.save(&path).unwrap();
1318
1319        // Verify VML part exists in the ZIP.
1320        let file = std::fs::File::open(&path).unwrap();
1321        let mut archive = zip::ZipArchive::new(file).unwrap();
1322
1323        let has_vml = (1..=10).any(|i| {
1324            archive
1325                .by_name(&format!("xl/drawings/vmlDrawing{i}.vml"))
1326                .is_ok()
1327        });
1328        assert!(has_vml, "should have a vmlDrawing file in the ZIP");
1329
1330        // Re-open and verify controls are preserved.
1331        let opts = crate::workbook::OpenOptions::new()
1332            .read_mode(crate::workbook::ReadMode::Eager)
1333            .aux_parts(crate::workbook::AuxParts::EagerLoad);
1334        let mut wb2 = Workbook::open_with_options(&path, &opts).unwrap();
1335        let controls = wb2.get_form_controls("Sheet1").unwrap();
1336        assert_eq!(controls.len(), 3);
1337        assert_eq!(controls[0].control_type, FormControlType::Button);
1338        assert_eq!(controls[0].text.as_deref(), Some("Submit"));
1339        assert_eq!(controls[0].macro_name.as_deref(), Some("Sheet1.OnSubmit"));
1340        assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1341        assert_eq!(controls[1].cell_link.as_deref(), Some("$D$4"));
1342        assert_eq!(controls[1].checked, Some(true));
1343        assert_eq!(controls[2].control_type, FormControlType::SpinButton);
1344        assert_eq!(controls[2].min_value, Some(0));
1345        assert_eq!(controls[2].max_value, Some(100));
1346    }
1347
1348    #[test]
1349    fn test_workbook_form_control_all_7_types() {
1350        use crate::workbook::Workbook;
1351
1352        let mut wb = Workbook::new();
1353        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Button"))
1354            .unwrap();
1355        wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Checkbox"))
1356            .unwrap();
1357        wb.add_form_control(
1358            "Sheet1",
1359            FormControlConfig {
1360                control_type: FormControlType::OptionButton,
1361                cell: "A5".to_string(),
1362                width: None,
1363                height: None,
1364                text: Some("Option".to_string()),
1365                macro_name: None,
1366                cell_link: None,
1367                checked: None,
1368                min_value: None,
1369                max_value: None,
1370                increment: None,
1371                page_increment: None,
1372                current_value: None,
1373                three_d: None,
1374            },
1375        )
1376        .unwrap();
1377        wb.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 10))
1378            .unwrap();
1379        wb.add_form_control("Sheet1", FormControlConfig::scroll_bar("E1", 0, 100))
1380            .unwrap();
1381        wb.add_form_control(
1382            "Sheet1",
1383            FormControlConfig {
1384                control_type: FormControlType::GroupBox,
1385                cell: "G1".to_string(),
1386                width: None,
1387                height: None,
1388                text: Some("Group".to_string()),
1389                macro_name: None,
1390                cell_link: None,
1391                checked: None,
1392                min_value: None,
1393                max_value: None,
1394                increment: None,
1395                page_increment: None,
1396                current_value: None,
1397                three_d: None,
1398            },
1399        )
1400        .unwrap();
1401        wb.add_form_control(
1402            "Sheet1",
1403            FormControlConfig {
1404                control_type: FormControlType::Label,
1405                cell: "I1".to_string(),
1406                width: None,
1407                height: None,
1408                text: Some("Label Text".to_string()),
1409                macro_name: None,
1410                cell_link: None,
1411                checked: None,
1412                min_value: None,
1413                max_value: None,
1414                increment: None,
1415                page_increment: None,
1416                current_value: None,
1417                three_d: None,
1418            },
1419        )
1420        .unwrap();
1421
1422        let controls = wb.get_form_controls("Sheet1").unwrap();
1423        assert_eq!(controls.len(), 7);
1424        assert_eq!(controls[0].control_type, FormControlType::Button);
1425        assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1426        assert_eq!(controls[2].control_type, FormControlType::OptionButton);
1427        assert_eq!(controls[3].control_type, FormControlType::SpinButton);
1428        assert_eq!(controls[4].control_type, FormControlType::ScrollBar);
1429        assert_eq!(controls[5].control_type, FormControlType::GroupBox);
1430        assert_eq!(controls[6].control_type, FormControlType::Label);
1431    }
1432
1433    #[test]
1434    fn test_workbook_form_control_with_comments() {
1435        use crate::workbook::Workbook;
1436        use tempfile::TempDir;
1437
1438        let dir = TempDir::new().unwrap();
1439        let path = dir.path().join("controls_and_comments.xlsx");
1440
1441        let mut wb = Workbook::new();
1442        wb.add_comment(
1443            "Sheet1",
1444            &crate::comment::CommentConfig {
1445                cell: "A1".to_string(),
1446                author: "Author".to_string(),
1447                text: "A comment".to_string(),
1448            },
1449        )
1450        .unwrap();
1451        wb.add_form_control("Sheet1", FormControlConfig::button("C1", "Button"))
1452            .unwrap();
1453        wb.save(&path).unwrap();
1454
1455        let mut wb2 = Workbook::open(&path).unwrap();
1456        let comments = wb2.get_comments("Sheet1").unwrap();
1457        assert_eq!(comments.len(), 1);
1458        let controls = wb2.get_form_controls("Sheet1").unwrap();
1459        assert_eq!(controls.len(), 1);
1460        assert_eq!(controls[0].control_type, FormControlType::Button);
1461    }
1462
1463    #[test]
1464    fn test_workbook_get_form_controls_empty() {
1465        use crate::workbook::Workbook;
1466
1467        let mut wb = Workbook::new();
1468        let controls = wb.get_form_controls("Sheet1").unwrap();
1469        assert!(controls.is_empty());
1470    }
1471
1472    #[test]
1473    fn test_workbook_get_form_controls_sheet_not_found() {
1474        use crate::workbook::Workbook;
1475
1476        let mut wb = Workbook::new();
1477        let result = wb.get_form_controls("NoSheet");
1478        assert!(result.is_err());
1479    }
1480
1481    #[test]
1482    fn test_open_file_get_form_controls_returns_existing() {
1483        use crate::workbook::Workbook;
1484        use crate::workbook::{AuxParts, OpenOptions, ReadMode};
1485        use tempfile::TempDir;
1486
1487        let dir = TempDir::new().unwrap();
1488        let path = dir.path().join("get_existing.xlsx");
1489
1490        let mut wb = Workbook::new();
1491        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Existing"))
1492            .unwrap();
1493        wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Check"))
1494            .unwrap();
1495        wb.save(&path).unwrap();
1496
1497        let opts = OpenOptions::new()
1498            .read_mode(ReadMode::Eager)
1499            .aux_parts(AuxParts::EagerLoad);
1500        let mut wb2 = Workbook::open_with_options(&path, &opts).unwrap();
1501        let controls = wb2.get_form_controls("Sheet1").unwrap();
1502        assert_eq!(controls.len(), 2);
1503        assert_eq!(controls[0].control_type, FormControlType::Button);
1504        assert_eq!(controls[0].text.as_deref(), Some("Existing"));
1505        assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1506        assert_eq!(controls[1].text.as_deref(), Some("Check"));
1507    }
1508
1509    #[test]
1510    fn test_open_file_add_form_control_preserves_existing() {
1511        use crate::workbook::Workbook;
1512        use crate::workbook::{AuxParts, OpenOptions, ReadMode};
1513        use tempfile::TempDir;
1514
1515        let dir = TempDir::new().unwrap();
1516        let path = dir.path().join("add_preserves.xlsx");
1517
1518        let mut wb = Workbook::new();
1519        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "First"))
1520            .unwrap();
1521        wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Second"))
1522            .unwrap();
1523        wb.save(&path).unwrap();
1524
1525        let opts = OpenOptions::new()
1526            .read_mode(ReadMode::Eager)
1527            .aux_parts(AuxParts::EagerLoad);
1528        let mut wb2 = Workbook::open_with_options(&path, &opts).unwrap();
1529        wb2.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 50))
1530            .unwrap();
1531
1532        let controls = wb2.get_form_controls("Sheet1").unwrap();
1533        assert_eq!(
1534            controls.len(),
1535            3,
1536            "old + new controls should all be present"
1537        );
1538        assert_eq!(controls[0].control_type, FormControlType::Button);
1539        assert_eq!(controls[0].text.as_deref(), Some("First"));
1540        assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1541        assert_eq!(controls[1].text.as_deref(), Some("Second"));
1542        assert_eq!(controls[2].control_type, FormControlType::SpinButton);
1543        assert_eq!(controls[2].min_value, Some(0));
1544        assert_eq!(controls[2].max_value, Some(50));
1545    }
1546
1547    #[test]
1548    fn test_open_file_delete_form_control_works() {
1549        use crate::workbook::Workbook;
1550        use crate::workbook::{AuxParts, OpenOptions, ReadMode};
1551        use tempfile::TempDir;
1552
1553        let dir = TempDir::new().unwrap();
1554        let path = dir.path().join("delete_works.xlsx");
1555
1556        let mut wb = Workbook::new();
1557        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "First"))
1558            .unwrap();
1559        wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Second"))
1560            .unwrap();
1561        wb.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 10))
1562            .unwrap();
1563        wb.save(&path).unwrap();
1564
1565        let opts = OpenOptions::new()
1566            .read_mode(ReadMode::Eager)
1567            .aux_parts(AuxParts::EagerLoad);
1568        let mut wb2 = Workbook::open_with_options(&path, &opts).unwrap();
1569        wb2.delete_form_control("Sheet1", 1).unwrap();
1570
1571        let controls = wb2.get_form_controls("Sheet1").unwrap();
1572        assert_eq!(controls.len(), 2);
1573        assert_eq!(controls[0].control_type, FormControlType::Button);
1574        assert_eq!(controls[1].control_type, FormControlType::SpinButton);
1575    }
1576
1577    #[test]
1578    fn test_open_file_modify_save_reopen_persistence() {
1579        use crate::workbook::Workbook;
1580        use crate::workbook::{AuxParts, OpenOptions, ReadMode};
1581        use tempfile::TempDir;
1582
1583        let dir = TempDir::new().unwrap();
1584        let path1 = dir.path().join("persistence_step1.xlsx");
1585        let path2 = dir.path().join("persistence_step2.xlsx");
1586
1587        let opts = OpenOptions::new()
1588            .read_mode(ReadMode::Eager)
1589            .aux_parts(AuxParts::EagerLoad);
1590
1591        // Step 1: Create with 2 controls.
1592        let mut wb = Workbook::new();
1593        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Button"))
1594            .unwrap();
1595        wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Check"))
1596            .unwrap();
1597        wb.save(&path1).unwrap();
1598
1599        // Step 2: Open, add one, delete one, save.
1600        let mut wb2 = Workbook::open_with_options(&path1, &opts).unwrap();
1601        wb2.add_form_control("Sheet1", FormControlConfig::scroll_bar("E1", 0, 100))
1602            .unwrap();
1603        wb2.delete_form_control("Sheet1", 0).unwrap();
1604        wb2.save(&path2).unwrap();
1605
1606        // Step 3: Re-open and verify.
1607        let mut wb3 = Workbook::open_with_options(&path2, &opts).unwrap();
1608        let controls = wb3.get_form_controls("Sheet1").unwrap();
1609        assert_eq!(controls.len(), 2);
1610        assert_eq!(controls[0].control_type, FormControlType::CheckBox);
1611        assert_eq!(controls[0].text.as_deref(), Some("Check"));
1612        assert_eq!(controls[1].control_type, FormControlType::ScrollBar);
1613        assert_eq!(controls[1].min_value, Some(0));
1614        assert_eq!(controls[1].max_value, Some(100));
1615    }
1616
1617    #[test]
1618    fn test_info_to_config_roundtrip() {
1619        let config = FormControlConfig {
1620            control_type: FormControlType::CheckBox,
1621            cell: "B2".to_string(),
1622            width: None,
1623            height: None,
1624            text: Some("Toggle".to_string()),
1625            macro_name: Some("MyMacro".to_string()),
1626            cell_link: Some("$D$1".to_string()),
1627            checked: Some(true),
1628            min_value: None,
1629            max_value: None,
1630            increment: None,
1631            page_increment: None,
1632            current_value: None,
1633            three_d: None,
1634        };
1635
1636        let vml = build_form_control_vml(&[config.clone()], 1025);
1637        let parsed = parse_form_controls(&vml);
1638        assert_eq!(parsed.len(), 1);
1639        let roundtripped = parsed[0].to_config();
1640        assert_eq!(roundtripped.control_type, config.control_type);
1641        assert_eq!(roundtripped.text, config.text);
1642        assert_eq!(roundtripped.macro_name, config.macro_name);
1643        assert_eq!(roundtripped.cell_link, config.cell_link);
1644        assert_eq!(roundtripped.checked, config.checked);
1645    }
1646
1647    #[test]
1648    fn test_strip_form_control_shapes_controls_only() {
1649        let vml = build_form_control_vml(
1650            &[
1651                FormControlConfig::button("A1", "Btn"),
1652                FormControlConfig::checkbox("A3", "Chk"),
1653            ],
1654            1025,
1655        );
1656        let result = strip_form_control_shapes_from_vml(vml.as_bytes());
1657        assert!(
1658            result.is_none(),
1659            "should return None when no comment shapes remain"
1660        );
1661    }
1662
1663    #[test]
1664    fn test_strip_form_control_shapes_mixed() {
1665        // Build VML with both a comment shape and a form control shape.
1666        let comment_vml = crate::vml::build_vml_drawing(&["A1"]);
1667        let controls = vec![FormControlConfig::button("C1", "Click")];
1668        let mixed = merge_vml_controls(comment_vml.as_bytes(), &controls, 1026);
1669
1670        let mixed_str = String::from_utf8_lossy(&mixed);
1671        assert!(mixed_str.contains("ObjectType=\"Note\""));
1672        assert!(mixed_str.contains("ObjectType=\"Button\""));
1673
1674        let stripped = strip_form_control_shapes_from_vml(&mixed).unwrap();
1675        let stripped_str = String::from_utf8(stripped).unwrap();
1676        assert!(
1677            stripped_str.contains("ObjectType=\"Note\""),
1678            "comment shapes should be preserved"
1679        );
1680        assert!(
1681            !stripped_str.contains("ObjectType=\"Button\""),
1682            "form control shapes should be removed"
1683        );
1684    }
1685
1686    #[test]
1687    fn test_hydration_does_not_duplicate_on_save() {
1688        use crate::workbook::Workbook;
1689        use crate::workbook::{AuxParts, OpenOptions, ReadMode};
1690        use tempfile::TempDir;
1691
1692        let dir = TempDir::new().unwrap();
1693        let path1 = dir.path().join("no_dup_step1.xlsx");
1694        let path2 = dir.path().join("no_dup_step2.xlsx");
1695
1696        let opts = OpenOptions::new()
1697            .read_mode(ReadMode::Eager)
1698            .aux_parts(AuxParts::EagerLoad);
1699
1700        let mut wb = Workbook::new();
1701        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn"))
1702            .unwrap();
1703        wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Chk"))
1704            .unwrap();
1705        wb.save(&path1).unwrap();
1706
1707        // Open and trigger hydration via get_form_controls (read-only).
1708        let mut wb2 = Workbook::open_with_options(&path1, &opts).unwrap();
1709        let controls = wb2.get_form_controls("Sheet1").unwrap();
1710        assert_eq!(controls.len(), 2);
1711
1712        // Save without adding or removing anything.
1713        wb2.save(&path2).unwrap();
1714
1715        // Re-open and verify no duplication.
1716        let mut wb3 = Workbook::open_with_options(&path2, &opts).unwrap();
1717        let controls3 = wb3.get_form_controls("Sheet1").unwrap();
1718        assert_eq!(
1719            controls3.len(),
1720            2,
1721            "control count must be stable after hydrate+save cycle"
1722        );
1723        assert_eq!(controls3[0].control_type, FormControlType::Button);
1724        assert_eq!(controls3[1].control_type, FormControlType::CheckBox);
1725    }
1726
1727    #[test]
1728    fn test_hydration_then_add_no_duplication() {
1729        use crate::workbook::Workbook;
1730        use crate::workbook::{AuxParts, OpenOptions, ReadMode};
1731        use tempfile::TempDir;
1732
1733        let dir = TempDir::new().unwrap();
1734        let path1 = dir.path().join("add_no_dup_step1.xlsx");
1735        let path2 = dir.path().join("add_no_dup_step2.xlsx");
1736
1737        let opts = OpenOptions::new()
1738            .read_mode(ReadMode::Eager)
1739            .aux_parts(AuxParts::EagerLoad);
1740
1741        let mut wb = Workbook::new();
1742        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Existing"))
1743            .unwrap();
1744        wb.save(&path1).unwrap();
1745
1746        // Open, add a new control, save.
1747        let mut wb2 = Workbook::open_with_options(&path1, &opts).unwrap();
1748        wb2.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "New"))
1749            .unwrap();
1750        wb2.save(&path2).unwrap();
1751
1752        // Re-open and verify exact expected count.
1753        let mut wb3 = Workbook::open_with_options(&path2, &opts).unwrap();
1754        let controls = wb3.get_form_controls("Sheet1").unwrap();
1755        assert_eq!(controls.len(), 2, "should have exactly 1 existing + 1 new");
1756        assert_eq!(controls[0].control_type, FormControlType::Button);
1757        assert_eq!(controls[0].text.as_deref(), Some("Existing"));
1758        assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1759        assert_eq!(controls[1].text.as_deref(), Some("New"));
1760    }
1761
1762    #[test]
1763    fn test_hydration_with_comments_no_duplication() {
1764        use crate::workbook::Workbook;
1765        use crate::workbook::{AuxParts, OpenOptions, ReadMode};
1766        use tempfile::TempDir;
1767
1768        let dir = TempDir::new().unwrap();
1769        let path1 = dir.path().join("comments_no_dup_step1.xlsx");
1770        let path2 = dir.path().join("comments_no_dup_step2.xlsx");
1771
1772        let opts = OpenOptions::new()
1773            .read_mode(ReadMode::Eager)
1774            .aux_parts(AuxParts::EagerLoad);
1775
1776        let mut wb = Workbook::new();
1777        wb.add_comment(
1778            "Sheet1",
1779            &crate::comment::CommentConfig {
1780                cell: "A1".to_string(),
1781                author: "Author".to_string(),
1782                text: "A comment".to_string(),
1783            },
1784        )
1785        .unwrap();
1786        wb.add_form_control("Sheet1", FormControlConfig::button("C1", "Btn"))
1787            .unwrap();
1788        wb.save(&path1).unwrap();
1789
1790        // Open, hydrate via get_form_controls, save.
1791        let mut wb2 = Workbook::open_with_options(&path1, &opts).unwrap();
1792        let controls = wb2.get_form_controls("Sheet1").unwrap();
1793        assert_eq!(controls.len(), 1);
1794        let comments = wb2.get_comments("Sheet1").unwrap();
1795        assert_eq!(comments.len(), 1);
1796        wb2.save(&path2).unwrap();
1797
1798        // Re-open and verify no duplication of either comments or controls.
1799        let mut wb3 = Workbook::open_with_options(&path2, &opts).unwrap();
1800        let controls3 = wb3.get_form_controls("Sheet1").unwrap();
1801        assert_eq!(
1802            controls3.len(),
1803            1,
1804            "form controls must not be duplicated when mixed with comments"
1805        );
1806        assert_eq!(controls3[0].control_type, FormControlType::Button);
1807        let comments3 = wb3.get_comments("Sheet1").unwrap();
1808        assert_eq!(comments3.len(), 1);
1809        assert_eq!(comments3[0].text, "A comment");
1810    }
1811
1812    #[test]
1813    fn test_multiple_hydrate_save_cycles_stable_count() {
1814        use crate::workbook::Workbook;
1815        use crate::workbook::{AuxParts, OpenOptions, ReadMode};
1816        use tempfile::TempDir;
1817
1818        let dir = TempDir::new().unwrap();
1819        let mut prev_path = dir.path().join("cycle_0.xlsx");
1820
1821        let opts = OpenOptions::new()
1822            .read_mode(ReadMode::Eager)
1823            .aux_parts(AuxParts::EagerLoad);
1824
1825        let mut wb = Workbook::new();
1826        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn"))
1827            .unwrap();
1828        wb.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 50))
1829            .unwrap();
1830        wb.save(&prev_path).unwrap();
1831
1832        // Run 3 open-hydrate-save cycles.
1833        for i in 1..=3 {
1834            let next_path = dir.path().join(format!("cycle_{i}.xlsx"));
1835            let mut wb_n = Workbook::open_with_options(&prev_path, &opts).unwrap();
1836            let controls = wb_n.get_form_controls("Sheet1").unwrap();
1837            assert_eq!(
1838                controls.len(),
1839                2,
1840                "cycle {i}: control count should remain 2"
1841            );
1842            wb_n.save(&next_path).unwrap();
1843            prev_path = next_path;
1844        }
1845    }
1846
1847    #[test]
1848    fn test_save_without_get_preserves_controls() {
1849        use crate::workbook::Workbook;
1850        use crate::workbook::{AuxParts, OpenOptions, ReadMode};
1851        use tempfile::TempDir;
1852
1853        let dir = TempDir::new().unwrap();
1854        let path1 = dir.path().join("no_get_step1.xlsx");
1855        let path2 = dir.path().join("no_get_step2.xlsx");
1856
1857        let opts = OpenOptions::new()
1858            .read_mode(ReadMode::Eager)
1859            .aux_parts(AuxParts::EagerLoad);
1860
1861        let mut wb = Workbook::new();
1862        wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn"))
1863            .unwrap();
1864        wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Chk"))
1865            .unwrap();
1866        wb.save(&path1).unwrap();
1867
1868        // Open and save immediately without calling get_form_controls.
1869        let wb2 = Workbook::open_with_options(&path1, &opts).unwrap();
1870        wb2.save(&path2).unwrap();
1871
1872        // Re-open and verify controls are preserved without duplication.
1873        let mut wb3 = Workbook::open_with_options(&path2, &opts).unwrap();
1874        let controls = wb3.get_form_controls("Sheet1").unwrap();
1875        assert_eq!(
1876            controls.len(),
1877            2,
1878            "controls must be preserved when saving without hydration"
1879        );
1880        assert_eq!(controls[0].control_type, FormControlType::Button);
1881        assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1882    }
1883}